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

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.
@@ -0,0 +1,785 @@
1
+ package com.bitmart.react.design
2
+
3
+ import android.content.res.ColorStateList
4
+ import android.graphics.Color
5
+ import android.graphics.PorterDuff
6
+ import android.graphics.drawable.Drawable
7
+ import android.util.Log
8
+ import android.view.ContextThemeWrapper
9
+ import android.view.View
10
+ import android.widget.FrameLayout
11
+ import android.widget.ProgressBar
12
+ import androidx.appcompat.widget.AppCompatButton
13
+ import androidx.core.widget.TextViewCompat
14
+ import coil.ImageLoader
15
+ import coil.decode.SvgDecoder
16
+ import coil.request.ImageRequest
17
+ import com.facebook.react.bridge.Arguments
18
+ import com.facebook.react.bridge.ReactContext
19
+ import com.facebook.react.common.MapBuilder
20
+ import com.facebook.react.uimanager.PixelUtil
21
+ import com.facebook.react.uimanager.SimpleViewManager
22
+ import com.facebook.react.uimanager.ThemedReactContext
23
+ import com.facebook.react.uimanager.annotations.ReactProp
24
+ import com.facebook.react.uimanager.events.RCTEventEmitter
25
+
26
+ /**
27
+ * PrimaryXLargeViewManager - 通用的原生按钮 ViewManager
28
+ *
29
+ * 使用原生 Button + 4参数构造函数(minSdk 24 >= API 21)
30
+ * 支持从 RN 侧动态传入样式名称(如 "Primary.XLarge", "Secondary.Medium" 等)
31
+ *
32
+ * 支持的样式:
33
+ * - Primary: XLarge, Large, Medium, Small, XSmall, XXSmall
34
+ * - Secondary: XLarge, Large, Medium, Small, XSmall, XXSmall
35
+ * - Green: XLarge, Large, Medium, Small, XSmall, XXSmall
36
+ * - Red: XLarge, Large, Medium, Small, XSmall, XXSmall
37
+ * - White: XLarge, Large, Medium, Small, XSmall, XXSmall
38
+ */
39
+ /**
40
+ * 自定义 FrameLayout:在 onLayout 中强制为 ProgressBar 指定尺寸并居中。
41
+ * 原因:RN/Yoga 只管理本节点尺寸,应用布局时可能覆盖或忽略子 View 的 LayoutParams,
42
+ * 导致 ProgressBar 被量成 0x0。此处不依赖 LayoutParams,用 tag 存进度条尺寸并手动 layout。
43
+ */
44
+ private class ButtonWithProgressLayout(context: android.content.Context) : FrameLayout(context) {
45
+
46
+ /** 进度条尺寸(px),由 ViewManager 通过 setTag 设置,onLayout 时用于强制 layout 第二子 View */
47
+ var progressBarSizePx: Int
48
+ get() = (getTag() as? Number)?.toInt() ?: 0
49
+ set(value) = setTag(value)
50
+
51
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
52
+ val w = r - l
53
+ val h = b - t
54
+ if (childCount < 2) {
55
+ super.onLayout(changed, l, t, r, b)
56
+ return
57
+ }
58
+ val button = getChildAt(0)
59
+ val progressBar = getChildAt(1)
60
+ val sizePx = progressBarSizePx.coerceAtLeast(0)
61
+ // 第一子 View:按钮 MATCH_PARENT,填满容器(支持 RN width: '100%' / flex: 1)
62
+ button.layout(0, 0, w, h)
63
+ // 第二子 View:ProgressBar,强制 sizePx x sizePx 居中(避免 Yoga/RN 导致 0x0)
64
+ if (sizePx > 0) {
65
+ val pbLeft = (w - sizePx) / 2
66
+ val pbTop = (h - sizePx) / 2
67
+ progressBar.layout(pbLeft, pbTop, pbLeft + sizePx, pbTop + sizePx)
68
+ } else {
69
+ progressBar.layout(0, 0, 0, 0)
70
+ }
71
+ }
72
+ }
73
+
74
+ class PrimaryXLargeViewManager : SimpleViewManager<FrameLayout>() {
75
+
76
+ companion object {
77
+ private const val TAG = "PrimaryXLargeViewManager"
78
+ const val REACT_CLASS = "PrimaryXLargeButton"
79
+ const val EVENT_ON_PRESS = "onPress"
80
+ const val EVENT_ON_SIZE_CHANGE = "onSizeChange"
81
+
82
+ /**
83
+ * 样式名称到资源 ID 的映射表
84
+ */
85
+ private val STYLE_MAP = mapOf(
86
+ // Primary 样式
87
+ "Primary.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_Button_Primary_XLarge,
88
+ "Primary.Large" to com.bitmart.design.R.style.Widget_BitMart4_Button_Primary_Large,
89
+ "Primary.Medium" to com.bitmart.design.R.style.Widget_BitMart4_Button_Primary_Medium,
90
+ "Primary.Small" to com.bitmart.design.R.style.Widget_BitMart4_Button_Primary_Small,
91
+ "Primary.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Primary_XSmall,
92
+ "Primary.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Primary_XXSmall,
93
+
94
+ // Secondary 样式
95
+ "Secondary.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_Button_Secondary_XLarge,
96
+ "Secondary.Large" to com.bitmart.design.R.style.Widget_BitMart4_Button_Secondary_Large,
97
+ "Secondary.Medium" to com.bitmart.design.R.style.Widget_BitMart4_Button_Secondary_Medium,
98
+ "Secondary.Small" to com.bitmart.design.R.style.Widget_BitMart4_Button_Secondary_Small,
99
+ "Secondary.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Secondary_XSmall,
100
+ "Secondary.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Secondary_XXSmall,
101
+
102
+ // Green 样式
103
+ "Green.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_Button_Green_XLarge,
104
+ "Green.Large" to com.bitmart.design.R.style.Widget_BitMart4_Button_Green_Large,
105
+ "Green.Medium" to com.bitmart.design.R.style.Widget_BitMart4_Button_Green_Medium,
106
+ "Green.Small" to com.bitmart.design.R.style.Widget_BitMart4_Button_Green_Small,
107
+ "Green.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Green_XSmall,
108
+ "Green.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Green_XXSmall,
109
+
110
+ // Red 样式
111
+ "Red.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_Button_Red_XLarge,
112
+ "Red.Large" to com.bitmart.design.R.style.Widget_BitMart4_Button_Red_Large,
113
+ "Red.Medium" to com.bitmart.design.R.style.Widget_BitMart4_Button_Red_Medium,
114
+ "Red.Small" to com.bitmart.design.R.style.Widget_BitMart4_Button_Red_Small,
115
+ "Red.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Red_XSmall,
116
+ "Red.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_Red_XXSmall,
117
+
118
+ // White 样式
119
+ "White.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_Button_White_XLarge,
120
+ "White.Large" to com.bitmart.design.R.style.Widget_BitMart4_Button_White_Large,
121
+ "White.Medium" to com.bitmart.design.R.style.Widget_BitMart4_Button_White_Medium,
122
+ "White.Small" to com.bitmart.design.R.style.Widget_BitMart4_Button_White_Small,
123
+ "White.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_White_XSmall,
124
+ "White.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_Button_White_XXSmall
125
+ )
126
+
127
+ /**
128
+ * 按钮尺寸对应的 ProgressBar 样式(与 fragment_buttons_primary.xml 一致)
129
+ * XLarge/Large/Medium -> Medium; Small -> Small; XSmall -> XSmall; XXSmall -> XXSmall
130
+ */
131
+ private val PROGRESS_STYLE_MAP = mapOf(
132
+ "Primary.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
133
+ "Primary.Large" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
134
+ "Primary.Medium" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
135
+ "Primary.Small" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Small,
136
+ "Primary.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XSmall,
137
+ "Primary.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XXSmall,
138
+ "Secondary.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
139
+ "Secondary.Large" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
140
+ "Secondary.Medium" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
141
+ "Secondary.Small" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Small,
142
+ "Secondary.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XSmall,
143
+ "Secondary.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XXSmall,
144
+ "Green.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
145
+ "Green.Large" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
146
+ "Green.Medium" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
147
+ "Green.Small" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Small,
148
+ "Green.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XSmall,
149
+ "Green.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XXSmall,
150
+ "Red.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
151
+ "Red.Large" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
152
+ "Red.Medium" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
153
+ "Red.Small" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Small,
154
+ "Red.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XSmall,
155
+ "Red.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XXSmall,
156
+ "White.XLarge" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
157
+ "White.Large" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
158
+ "White.Medium" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium,
159
+ "White.Small" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Small,
160
+ "White.XSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XSmall,
161
+ "White.XXSmall" to com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_XXSmall
162
+ )
163
+
164
+ fun getStyleResId(styleName: String?): Int {
165
+ return STYLE_MAP[styleName] ?: com.bitmart.design.R.style.Widget_BitMart4_Button_Primary_XLarge
166
+ }
167
+
168
+ fun getProgressBarStyleResId(styleName: String?): Int {
169
+ return PROGRESS_STYLE_MAP[styleName] ?: com.bitmart.design.R.style.Widget_BitMart4_ProgressBar_Medium
170
+ }
171
+
172
+ /** 按钮尺寸对应的 ProgressBar 尺寸 dimen(与 design 库 dimens.xml 一致)*/
173
+ private val PROGRESS_SIZE_DIMEN_MAP = mapOf(
174
+ "Primary.XLarge" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
175
+ "Primary.Large" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
176
+ "Primary.Medium" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
177
+ "Primary.Small" to com.bitmart.design.R.dimen.bm4_progress_bar_size_small,
178
+ "Primary.XSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_x_small,
179
+ "Primary.XXSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_xx_small,
180
+ "Secondary.XLarge" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
181
+ "Secondary.Large" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
182
+ "Secondary.Medium" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
183
+ "Secondary.Small" to com.bitmart.design.R.dimen.bm4_progress_bar_size_small,
184
+ "Secondary.XSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_x_small,
185
+ "Secondary.XXSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_xx_small,
186
+ "Green.XLarge" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
187
+ "Green.Large" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
188
+ "Green.Medium" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
189
+ "Green.Small" to com.bitmart.design.R.dimen.bm4_progress_bar_size_small,
190
+ "Green.XSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_x_small,
191
+ "Green.XXSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_xx_small,
192
+ "Red.XLarge" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
193
+ "Red.Large" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
194
+ "Red.Medium" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
195
+ "Red.Small" to com.bitmart.design.R.dimen.bm4_progress_bar_size_small,
196
+ "Red.XSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_x_small,
197
+ "Red.XXSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_xx_small,
198
+ "White.XLarge" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
199
+ "White.Large" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
200
+ "White.Medium" to com.bitmart.design.R.dimen.bm4_progress_bar_size_medium,
201
+ "White.Small" to com.bitmart.design.R.dimen.bm4_progress_bar_size_small,
202
+ "White.XSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_x_small,
203
+ "White.XXSmall" to com.bitmart.design.R.dimen.bm4_progress_bar_size_xx_small
204
+ )
205
+
206
+ fun getProgressBarSizePx(context: android.content.Context, styleName: String?): Int {
207
+ val dimenId = PROGRESS_SIZE_DIMEN_MAP[styleName] ?: com.bitmart.design.R.dimen.bm4_progress_bar_size_medium
208
+ return context.resources.getDimensionPixelSize(dimenId)
209
+ }
210
+ }
211
+
212
+ // 保存按钮的样式名称和 ReactContext
213
+ private val buttonStyleMap = mutableMapOf<AppCompatButton, String>()
214
+ private val buttonContextMap = mutableMapOf<AppCompatButton, ReactContext>()
215
+ // 保存按钮的图标信息
216
+ private val buttonIconMap = mutableMapOf<AppCompatButton, IconInfo>()
217
+ // 包装视图 -> 按钮 / 进度条(根视图为 FrameLayout,内含 Button + ProgressBar)
218
+ private val wrapperToButtonMap = mutableMapOf<FrameLayout, AppCompatButton>()
219
+ private val wrapperToProgressBarMap = mutableMapOf<FrameLayout, ProgressBar>()
220
+ private val buttonToWrapperMap = mutableMapOf<AppCompatButton, FrameLayout>()
221
+
222
+ /**
223
+ * 图标信息数据类
224
+ */
225
+ private data class IconInfo(
226
+ var svgString: String? = null,
227
+ var iconColor: String = "#ffffff",
228
+ var iconPosition: String = "leading"
229
+ )
230
+
231
+ override fun getName(): String = REACT_CLASS
232
+
233
+ override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout {
234
+ // 默认使用 Primary.XLarge 样式(保持向后兼容)
235
+ val defaultStyleName = "Primary.XLarge"
236
+ val styleResId = getStyleResId(defaultStyleName)
237
+ val progressStyleResId = getProgressBarStyleResId(defaultStyleName)
238
+
239
+ // 使用 ContextThemeWrapper 让 AppCompatButton 自动应用样式
240
+ val themedContext = ContextThemeWrapper(reactContext, styleResId)
241
+ val button = AppCompatButton(themedContext)
242
+ button.isAllCaps = false
243
+ button.gravity = android.view.Gravity.CENTER
244
+ // 保存样式名称和原始 ReactContext
245
+ buttonStyleMap[button] = defaultStyleName
246
+ buttonContextMap[button] = reactContext
247
+
248
+ // 手动应用背景和最小尺寸设置
249
+ applyBackground(button, styleResId)
250
+
251
+ // 进度条:使用 design 库的 ProgressBar 样式(与 fragment_buttons_primary.xml 一致)
252
+ val progressThemedContext = ContextThemeWrapper(reactContext, progressStyleResId)
253
+ val progressBar = ProgressBar(progressThemedContext, null, 0, progressStyleResId).apply {
254
+ visibility = View.GONE
255
+ isIndeterminate = true
256
+ // 尺寸样式可能未继承 indeterminateTint,显式设置白色以保证在彩色按钮上可见
257
+ indeterminateTintList = try {
258
+ ColorStateList.valueOf(reactContext.getColor(com.bitmart.design.R.color.bm4_btn_text))
259
+ } catch (e: Exception) {
260
+ ColorStateList.valueOf(Color.WHITE)
261
+ }
262
+ }
263
+ val progressSizePx = getProgressBarSizePx(reactContext, defaultStyleName)
264
+ // 使用自定义 Layout:在 onLayout 中强制为 ProgressBar 指定尺寸,避免 Yoga/RN 覆盖子 View LayoutParams 导致 0x0
265
+ // 按钮使用 MATCH_PARENT 填满容器,以便 RN 侧设置 width: '100%' / flex: 1 时能全宽或均分(Footer 底部按钮)
266
+ val wrapper = ButtonWithProgressLayout(reactContext).apply {
267
+ progressBarSizePx = progressSizePx
268
+ addView(button, FrameLayout.LayoutParams(
269
+ FrameLayout.LayoutParams.MATCH_PARENT,
270
+ FrameLayout.LayoutParams.MATCH_PARENT
271
+ ).apply { gravity = android.view.Gravity.CENTER })
272
+ addView(progressBar, FrameLayout.LayoutParams(0, 0))
273
+ }
274
+
275
+ wrapperToButtonMap[wrapper] = button
276
+ wrapperToProgressBarMap[wrapper] = progressBar
277
+ buttonToWrapperMap[button] = wrapper
278
+
279
+ return wrapper
280
+ }
281
+
282
+ /**
283
+ * 从包装视图获取按钮(供各 @ReactProp 使用)
284
+ */
285
+ private fun getButton(view: View): AppCompatButton? {
286
+ return (view as? FrameLayout)?.let { wrapperToButtonMap[it] }
287
+ }
288
+
289
+ /**
290
+ * 从包装视图获取进度条
291
+ */
292
+ private fun getProgressBar(view: View): ProgressBar? {
293
+ return (view as? FrameLayout)?.let { wrapperToProgressBarMap[it] }
294
+ }
295
+
296
+ /**
297
+ * 根据 styleName 更新进度条样式(尺寸变化时需同步 ProgressBar 尺寸)
298
+ */
299
+ private fun updateProgressBarStyle(wrapper: FrameLayout, styleName: String) {
300
+ val progressBar = wrapperToProgressBarMap[wrapper] ?: return
301
+ val progressStyleResId = getProgressBarStyleResId(styleName)
302
+ val progressThemedContext = ContextThemeWrapper(wrapper.context, progressStyleResId)
303
+ val newProgressBar = ProgressBar(progressThemedContext, null, 0, progressStyleResId).apply {
304
+ visibility = progressBar.visibility
305
+ isIndeterminate = true
306
+ indeterminateTintList = try {
307
+ ColorStateList.valueOf(wrapper.context.getColor(com.bitmart.design.R.color.bm4_btn_text))
308
+ } catch (e: Exception) {
309
+ ColorStateList.valueOf(Color.WHITE)
310
+ }
311
+ }
312
+ val progressSizePx = getProgressBarSizePx(wrapper.context, styleName)
313
+ val index = wrapper.indexOfChild(progressBar)
314
+ wrapper.removeView(progressBar)
315
+ wrapper.addView(newProgressBar, index, FrameLayout.LayoutParams(0, 0))
316
+ wrapperToProgressBarMap[wrapper] = newProgressBar
317
+ (wrapper as? ButtonWithProgressLayout)?.progressBarSizePx = progressSizePx
318
+ }
319
+
320
+ /**
321
+ * 设置按钮文本
322
+ */
323
+ @ReactProp(name = "text")
324
+ fun setText(view: View, text: String?) {
325
+ val button = getButton(view) ?: return
326
+ val oldText = button.text.toString()
327
+ val newText = text ?: ""
328
+
329
+ button.text = newText
330
+
331
+ // 文本变化后,测量并发送尺寸
332
+ if (oldText != newText) {
333
+ measureAndSendSize(view, button)
334
+ }
335
+ }
336
+
337
+ /**
338
+ * 设置样式名称(动态切换样式)
339
+ * @param styleName 样式名称,如 "Primary.XLarge", "Secondary.Medium" 等
340
+ */
341
+ @ReactProp(name = "styleName")
342
+ fun setStyleName(view: View, styleName: String?) {
343
+ val button = getButton(view) ?: return
344
+ val wrapper = view as? FrameLayout ?: return
345
+ if (styleName.isNullOrEmpty()) {
346
+ return
347
+ }
348
+
349
+ val currentStyle = buttonStyleMap[button]
350
+ if (currentStyle == styleName) {
351
+ return
352
+ }
353
+
354
+ val styleResId = getStyleResId(styleName)
355
+
356
+ if (STYLE_MAP.containsKey(styleName)) {
357
+ // 使用新的简化方法:重新应用完整主题
358
+ applyCompleteStyle(button, styleResId, styleName)
359
+ updateProgressBarStyle(wrapper, styleName)
360
+ measureAndSendSize(view, button)
361
+ }
362
+ }
363
+
364
+ /**
365
+ * 测量按钮尺寸并发送给 RN(使用包装视图的 id 发送事件)
366
+ */
367
+ private fun measureAndSendSize(view: View, button: AppCompatButton) {
368
+ val wrapper = view as? FrameLayout ?: return
369
+ button.post {
370
+ // 使用 WRAP_CONTENT 测量按钮的内在尺寸
371
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
372
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
373
+ button.measure(widthMeasureSpec, heightMeasureSpec)
374
+
375
+ val measuredWidth = button.measuredWidth
376
+ val measuredHeight = button.measuredHeight
377
+
378
+ // 如果 measure 返回 0,使用文字测量 + padding 作为备选方案
379
+ val finalWidth = if (measuredWidth > 0) {
380
+ measuredWidth
381
+ } else {
382
+ val textWidth = button.paint.measureText(button.text.toString())
383
+ (textWidth + button.paddingStart + button.paddingEnd).toInt()
384
+ }
385
+ val finalHeight = if (measuredHeight > 0) measuredHeight else button.height
386
+
387
+ // 转换为 dp
388
+ val widthDp = PixelUtil.toDIPFromPixel(finalWidth.toFloat())
389
+ val heightDp = PixelUtil.toDIPFromPixel(finalHeight.toFloat())
390
+
391
+ // 发送尺寸变化事件给 RN(使用包装视图 id)
392
+ val reactContext = buttonContextMap[button]
393
+ if (reactContext != null) {
394
+ val event = Arguments.createMap().apply {
395
+ putDouble("width", widthDp.toDouble())
396
+ putDouble("height", heightDp.toDouble())
397
+ }
398
+ reactContext.getJSModule(RCTEventEmitter::class.java)
399
+ .receiveEvent(wrapper.id, EVENT_ON_SIZE_CHANGE, event)
400
+ }
401
+ }
402
+ }
403
+
404
+ /**
405
+ * 应用背景和最小尺寸设置
406
+ * AppCompatButton 使用 ContextThemeWrapper 后不会自动应用背景,需要手动设置
407
+ */
408
+ private fun applyBackground(button: AppCompatButton, styleResId: Int) {
409
+ try {
410
+ val context = button.context
411
+ val typedArray = context.obtainStyledAttributes(
412
+ styleResId,
413
+ intArrayOf(android.R.attr.background)
414
+ )
415
+
416
+ // 应用背景(AppCompatButton 需要手动设置)
417
+ typedArray.getDrawable(0)?.let {
418
+ button.background = it
419
+ }
420
+
421
+ typedArray.recycle()
422
+
423
+ // 强制设置 minWidth 和 minHeight 为 0,让按钮宽度自适应文字
424
+ button.minimumWidth = 0
425
+ button.minimumHeight = 0
426
+ } catch (e: Exception) {
427
+ Log.e(TAG, "Error applying background", e)
428
+ }
429
+ }
430
+
431
+ /**
432
+ * 🆕 简化的完整样式应用方法 - 自动应用所有样式,无需手动解析
433
+ * 通过创建临时按钮获取完整样式,然后复制到目标按钮
434
+ */
435
+ private fun applyCompleteStyle(button: AppCompatButton, styleResId: Int, styleName: String) {
436
+ try {
437
+ // 保存当前按钮状态
438
+ val currentText = button.text
439
+ val currentEnabled = button.isEnabled
440
+ val originalContext = buttonContextMap[button] ?: return
441
+
442
+ // 创建一个临时按钮来获取完整的样式属性
443
+ val themedContext = ContextThemeWrapper(originalContext, styleResId)
444
+ val tempButton = AppCompatButton(themedContext)
445
+
446
+ // 从临时按钮复制所有样式属性到当前按钮
447
+ copyButtonStyle(tempButton, button)
448
+
449
+ // 复用现有的背景应用方法(确保背景正确生效,并自动设置最小尺寸)
450
+ applyBackground(button, styleResId)
451
+
452
+ // 手动设置其他必要属性
453
+ button.isAllCaps = false
454
+
455
+ // 恢复按钮状态
456
+ button.text = currentText
457
+ button.isEnabled = currentEnabled
458
+
459
+ // 强制刷新视图以确保样式变化生效
460
+ button.invalidate()
461
+ button.requestLayout()
462
+
463
+ // 更新样式映射
464
+ buttonStyleMap[button] = styleName
465
+
466
+ } catch (e: Exception) {
467
+ // 如果新方法失败,回退到原来的方法
468
+ // Log.w(TAG, "Failed to apply complete style, falling back to manual approach", e)
469
+ // applyStyleToButton(button, styleResId)
470
+ // buttonStyleMap[button] = styleName
471
+ }
472
+ }
473
+
474
+ /**
475
+ * 🆕 从源按钮复制完整样式到目标按钮
476
+ */
477
+ private fun copyButtonStyle(sourceButton: AppCompatButton, targetButton: AppCompatButton) {
478
+ // 复制文本样式
479
+ targetButton.setTextColor(sourceButton.textColors)
480
+ targetButton.textSize = sourceButton.textSize / targetButton.resources.displayMetrics.scaledDensity
481
+ targetButton.typeface = sourceButton.typeface
482
+
483
+ // 复制 padding
484
+ targetButton.setPadding(
485
+ sourceButton.paddingStart,
486
+ sourceButton.paddingTop,
487
+ sourceButton.paddingEnd,
488
+ sourceButton.paddingBottom
489
+ )
490
+
491
+ // 复制 drawable padding
492
+ targetButton.compoundDrawablePadding = sourceButton.compoundDrawablePadding
493
+
494
+ // 复制 compound drawables 和 tint
495
+ val drawables = sourceButton.compoundDrawables
496
+ targetButton.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3])
497
+ if (sourceButton.compoundDrawableTintList != null) {
498
+ targetButton.compoundDrawableTintList = sourceButton.compoundDrawableTintList
499
+ }
500
+
501
+ // 注意:背景通过单独的 applyBackground 方法处理,确保正确应用
502
+ }
503
+
504
+ /**
505
+ * ⚠️ 原有的手动解析样式方法(作为备用方案)
506
+ *
507
+ * 注意:这个方法从按钮样式的继承层次中提取属性:
508
+ * 1. Widget.BitMart4.Button.Primary.XLarge (具体尺寸)
509
+ * 2. Widget.BitMart4.Button.Primary (类型样式)
510
+ * 3. Widget.BitMart4.Button (基础样式)
511
+ *
512
+ * 当简化方法失败时使用,需要手动解析每个样式属性
513
+ */
514
+ private fun applyStyleToButton(button: AppCompatButton, styleResId: Int) {
515
+ try {
516
+ val context = button.context
517
+
518
+ // 获取所有需要的属性
519
+ val typedArray = context.obtainStyledAttributes(
520
+ styleResId,
521
+ intArrayOf(
522
+ android.R.attr.background, // 0
523
+ android.R.attr.textColor, // 1
524
+ android.R.attr.paddingStart, // 2
525
+ android.R.attr.paddingEnd, // 3
526
+ android.R.attr.textAppearance, // 4
527
+ android.R.attr.drawablePadding, // 5
528
+ android.R.attr.drawableTint // 6 - AppCompatButton 支持
529
+ )
530
+ )
531
+
532
+ // 应用背景(AppCompatButton 需要单独设置)
533
+ typedArray.getDrawable(0)?.let {
534
+ button.background = it
535
+ }
536
+
537
+ // 获取 textAppearance(每个尺寸都有自己的 textAppearance)
538
+ val textAppearanceResId = typedArray.getResourceId(4, -1)
539
+
540
+ // 获取样式中定义的 textColor(在 Primary/Secondary/等父样式中)
541
+ val textColorFromStyle = typedArray.getColorStateList(1)
542
+
543
+ // 应用 textAppearance(包含 fontFamily, textSize, fontWeight)
544
+ if (textAppearanceResId != -1) {
545
+ button.setTextAppearance(textAppearanceResId)
546
+ }
547
+
548
+ // 显式设置 textAllCaps = false(BitMart 按钮不使用大写)
549
+ button.isAllCaps = false
550
+
551
+ // 在 textAppearance 之后,强制应用样式中定义的 textColor
552
+ // 这样可以覆盖 textAppearance 中可能包含的默认颜色
553
+ if (textColorFromStyle != null) {
554
+ button.setTextColor(textColorFromStyle)
555
+ }
556
+
557
+ // 应用 padding
558
+ val paddingStart = typedArray.getDimensionPixelSize(2, button.paddingStart)
559
+ val paddingEnd = typedArray.getDimensionPixelSize(3, button.paddingEnd)
560
+ button.setPadding(paddingStart, button.paddingTop, paddingEnd, button.paddingBottom)
561
+
562
+ // 应用 drawablePadding(图标和文字之间的间距)
563
+ val drawablePadding = typedArray.getDimensionPixelSize(5, 0)
564
+ if (drawablePadding > 0) {
565
+ button.compoundDrawablePadding = drawablePadding
566
+ }
567
+
568
+ // 应用 drawableTint(AppCompatButton 支持)
569
+ val drawableTint = typedArray.getColorStateList(6)
570
+ if (drawableTint != null) {
571
+ TextViewCompat.setCompoundDrawableTintList(button, drawableTint)
572
+ }
573
+
574
+ typedArray.recycle()
575
+
576
+ // 强制设置 minWidth 和 minHeight 为 0,确保按钮宽度能根据文字内容自适应
577
+ button.minimumWidth = 0
578
+ button.minimumHeight = 0
579
+ } catch (e: Exception) {
580
+ Log.e(TAG, "Error applying style", e)
581
+ }
582
+ }
583
+
584
+ /**
585
+ * 设置启用状态
586
+ */
587
+ @ReactProp(name = "enabled", defaultBoolean = true)
588
+ fun setEnabled(view: View, enabled: Boolean) {
589
+ getButton(view)?.let {
590
+ it.isEnabled = enabled
591
+ it.refreshDrawableState()
592
+ }
593
+ }
594
+
595
+ /**
596
+ * 是否显示进度条(加载状态)。为 true 时:按钮背景移到 wrapper 上(与 ButtonContainer.migrateAttrsFrom 一致),
597
+ * 按钮 INVISIBLE,ProgressBar 叠在按钮形状上;为 false 时恢复。
598
+ * 参考 fragment_buttons_primary.xml 中 ButtonContainer + ProgressBar。
599
+ */
600
+ @ReactProp(name = "loading", defaultBoolean = false)
601
+ fun setLoading(view: View, loading: Boolean) {
602
+ val wrapper = view as? FrameLayout
603
+ val button = getButton(view)
604
+ val progressBar = getProgressBar(view)
605
+ if (button == null || progressBar == null) return
606
+ if (loading) {
607
+ // 与 ButtonContainer 一致:把按钮的 background 移到 wrapper,这样 loading 时仍显示按钮形状,ProgressBar 叠在上面
608
+ val bg = button.background
609
+ if (bg != null && wrapper != null) {
610
+ wrapper.background = bg
611
+ button.background = null
612
+ }
613
+ button.visibility = View.INVISIBLE
614
+ progressBar.visibility = View.VISIBLE
615
+ } else {
616
+ // 恢复:把 background 从 wrapper 移回按钮(仅当 wrapper 当前持有 background 时,避免初始 setLoading(false) 清空按钮)
617
+ if (wrapper != null && wrapper.background != null) {
618
+ button.background = wrapper.background
619
+ wrapper.background = null
620
+ }
621
+ button.visibility = View.VISIBLE
622
+ progressBar.visibility = View.GONE
623
+ }
624
+ }
625
+
626
+ /**
627
+ * 设置SVG图标字符串
628
+ */
629
+ @ReactProp(name = "iconSvgString")
630
+ fun setIconSvgString(view: View, svgString: String?) {
631
+ val button = getButton(view) ?: return
632
+ val iconInfo = buttonIconMap.getOrPut(button) { IconInfo() }
633
+ iconInfo.svgString = svgString
634
+ loadAndApplySvgIcon(button)
635
+ }
636
+
637
+ /**
638
+ * 设置图标颜色
639
+ */
640
+ @ReactProp(name = "iconColor")
641
+ fun setIconColor(view: View, color: String?) {
642
+ val button = getButton(view) ?: return
643
+ val iconInfo = buttonIconMap.getOrPut(button) { IconInfo() }
644
+ iconInfo.iconColor = color ?: "#ffffff"
645
+ loadAndApplySvgIcon(button)
646
+ }
647
+
648
+ /**
649
+ * 设置图标位置
650
+ */
651
+ @ReactProp(name = "iconPosition")
652
+ fun setIconPosition(view: View, position: String?) {
653
+ val button = getButton(view) ?: return
654
+ val iconInfo = buttonIconMap.getOrPut(button) { IconInfo() }
655
+ iconInfo.iconPosition = position ?: "leading"
656
+ loadAndApplySvgIcon(button)
657
+ }
658
+
659
+ /**
660
+ * 使用Coil加载并应用SVG图标
661
+ */
662
+ private fun loadAndApplySvgIcon(button: AppCompatButton) {
663
+ val iconInfo = buttonIconMap[button] ?: return
664
+ val svgString = iconInfo.svgString
665
+
666
+ if (svgString.isNullOrEmpty() || iconInfo.iconPosition == "none") {
667
+ button.setCompoundDrawables(null, null, null, null)
668
+ buttonToWrapperMap[button]?.let { measureAndSendSize(it, button) }
669
+ return
670
+ }
671
+
672
+ try {
673
+ val context = button.context
674
+ val styleName = buttonStyleMap[button] ?: "Primary.XLarge"
675
+ val iconSizePx = getIconSizeForStyle(styleName)
676
+
677
+ // 创建支持 SVG 的 ImageLoader(含 data:image/svg+xml;utf8 的 Fetcher)
678
+ val imageLoader = ImageLoader.Builder(context)
679
+ .components {
680
+ add(DataUriFetcher.Factory())
681
+ add(SvgDecoder.Factory())
682
+ }
683
+ .build()
684
+
685
+ // 使用 SvgStringData 直接传 SVG 字符串,避免 Uri 解析截断 fill="#ffffff" 等
686
+ val request = ImageRequest.Builder(context)
687
+ .data(SvgStringData(svgString))
688
+ .size(iconSizePx, iconSizePx)
689
+ .target { drawable ->
690
+ try {
691
+ val colorStr = iconInfo.iconColor
692
+ if (!colorStr.isNullOrEmpty()) {
693
+ val color = Color.parseColor(colorStr)
694
+ drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN)
695
+ }
696
+ } catch (e: Exception) {
697
+ Log.e(TAG, "Failed to parse color: ${iconInfo.iconColor}", e)
698
+ }
699
+ drawable.setBounds(0, 0, iconSizePx, iconSizePx)
700
+ applyIconToButton(button, drawable, iconInfo.iconPosition)
701
+ buttonToWrapperMap[button]?.let { measureAndSendSize(it, button) }
702
+ }
703
+ .listener(
704
+ onError = { _, result ->
705
+ Log.e(TAG, "Coil load error: ${result.throwable.message}", result.throwable)
706
+ }
707
+ )
708
+ .build()
709
+
710
+ // 执行请求
711
+ imageLoader.enqueue(request)
712
+
713
+ } catch (e: Exception) {
714
+ Log.e(TAG, "Error loading SVG icon", e)
715
+ button.setCompoundDrawables(null, null, null, null)
716
+ }
717
+ }
718
+
719
+ /**
720
+ * 应用图标到按钮
721
+ */
722
+ private fun applyIconToButton(button: AppCompatButton, drawable: Drawable, position: String) {
723
+ when (position) {
724
+ "leading" -> button.setCompoundDrawables(drawable, null, null, null)
725
+ "trailing" -> button.setCompoundDrawables(null, null, drawable, null)
726
+ else -> button.setCompoundDrawables(drawable, null, null, null)
727
+ }
728
+ }
729
+
730
+ /**
731
+ * 根据样式名称获取图标大小(单位:px)
732
+ */
733
+ private fun getIconSizeForStyle(styleName: String): Int {
734
+ return when {
735
+ styleName.contains("XLarge") || styleName.contains("Large") -> dpToPx(20f)
736
+ styleName.contains("Medium") || styleName.contains("Small") -> dpToPx(16f)
737
+ styleName.contains("XSmall") || styleName.contains("XXSmall") -> dpToPx(14f)
738
+ else -> dpToPx(20f)
739
+ }
740
+ }
741
+
742
+ /**
743
+ * dp 转 px
744
+ */
745
+ private fun dpToPx(dp: Float): Int {
746
+ val density = android.content.res.Resources.getSystem().displayMetrics.density
747
+ return (dp * density + 0.5f).toInt()
748
+ }
749
+
750
+ /**
751
+ * 导出事件映射
752
+ */
753
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
754
+ return MapBuilder.builder<String, Any>()
755
+ .put(EVENT_ON_PRESS, MapBuilder.of("registrationName", EVENT_ON_PRESS))
756
+ .put(EVENT_ON_SIZE_CHANGE, MapBuilder.of("registrationName", EVENT_ON_SIZE_CHANGE))
757
+ .build()
758
+ }
759
+
760
+ /**
761
+ * 添加点击事件监听(点击在按钮上,事件使用包装视图 id)
762
+ */
763
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: FrameLayout) {
764
+ val button = getButton(view) ?: return
765
+ button.setOnClickListener {
766
+ reactContext.getJSModule(RCTEventEmitter::class.java)
767
+ .receiveEvent(view.id, EVENT_ON_PRESS, null)
768
+ }
769
+ }
770
+
771
+ /**
772
+ * 清理资源
773
+ */
774
+ override fun onDropViewInstance(view: FrameLayout) {
775
+ super.onDropViewInstance(view)
776
+ val button = wrapperToButtonMap.remove(view)
777
+ wrapperToProgressBarMap.remove(view)
778
+ if (button != null) {
779
+ buttonToWrapperMap.remove(button)
780
+ buttonStyleMap.remove(button)
781
+ buttonContextMap.remove(button)
782
+ buttonIconMap.remove(button)
783
+ }
784
+ }
785
+ }