@onekeyfe/react-native-pager-view 3.0.119 → 3.0.121

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,505 @@
1
+ package com.reactnativepagerview
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.Typeface
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.text.TextUtils
8
+ import android.util.TypedValue
9
+ import android.view.Gravity
10
+ import android.view.MotionEvent
11
+ import android.view.View
12
+ import android.view.ViewGroup
13
+ import android.widget.FrameLayout
14
+ import android.widget.HorizontalScrollView
15
+ import android.widget.TextView
16
+ import androidx.core.graphics.ColorUtils
17
+ import org.json.JSONArray
18
+ import org.json.JSONObject
19
+ import kotlin.math.ceil
20
+ import kotlin.math.floor
21
+ import kotlin.math.max
22
+ import kotlin.math.min
23
+ import kotlin.math.roundToInt
24
+
25
+ internal data class CollapsiblePagerNativeHeaderItem(
26
+ val key: String,
27
+ val title: String,
28
+ val accessibilityLabel: String,
29
+ val testID: String?,
30
+ )
31
+
32
+ private fun parseHeaderItems(value: String?): List<CollapsiblePagerNativeHeaderItem> {
33
+ if (value.isNullOrEmpty()) return emptyList()
34
+ return runCatching {
35
+ val array = JSONArray(value)
36
+ List(array.length()) { index ->
37
+ val item = array.optJSONObject(index) ?: JSONObject()
38
+ val title = item.optString("title")
39
+ CollapsiblePagerNativeHeaderItem(
40
+ key = item.optString("key"),
41
+ title = title,
42
+ accessibilityLabel = item.optString("accessibilityLabel", title),
43
+ testID = item.optString("testID").takeIf(String::isNotEmpty),
44
+ )
45
+ }
46
+ }.getOrDefault(emptyList())
47
+ }
48
+
49
+ private class CollapsiblePagerHorizontalItemsView(
50
+ context: Context,
51
+ private val showsProgressIndicator: Boolean,
52
+ ) : HorizontalScrollView(context) {
53
+ private val density = resources.displayMetrics.density
54
+ private val content = FrameLayout(context)
55
+ private val indicator = View(context)
56
+ private val indicatorBackground = GradientDrawable().apply {
57
+ shape = GradientDrawable.RECTANGLE
58
+ }
59
+ private val buttons = ArrayList<TextView>()
60
+ private val itemFrames = ArrayList<IntArray>()
61
+ private var items: List<CollapsiblePagerNativeHeaderItem> = emptyList()
62
+ private var selectedKey = ""
63
+ private var rowHeightPx = dp(if (showsProgressIndicator) 44.0 else 42.0)
64
+ private var contentPaddingPx = dp(20.0)
65
+ private var itemSpacingPx = dp(8.0)
66
+ private var fontSize = if (showsProgressIndicator) 16.0 else 14.0
67
+ private var fontFamily: String? = null
68
+ private var activeTextColor = Color.BLACK
69
+ private var inactiveTextColor = Color.GRAY
70
+ private var selectedBackgroundColor = Color.TRANSPARENT
71
+ private var indicatorColor = Color.BLACK
72
+ private var indicatorHeightPx = dp(2.0)
73
+ private var indicatorBottomPx = 0
74
+ private var progress = 0f
75
+ private var centerSelectionWhenIdle = true
76
+ private var isTouchDragging = false
77
+
78
+ var onItemPress: ((Int, String) -> Unit)? = null
79
+
80
+ init {
81
+ isHorizontalScrollBarEnabled = false
82
+ isVerticalScrollBarEnabled = false
83
+ isFillViewport = true
84
+ overScrollMode = OVER_SCROLL_NEVER
85
+ clipToPadding = false
86
+ indicator.background = indicatorBackground
87
+ content.addView(indicator, FrameLayout.LayoutParams(0, 0))
88
+ addView(
89
+ content,
90
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT),
91
+ )
92
+ }
93
+
94
+ private fun dp(value: Double): Int = (value * density).roundToInt()
95
+
96
+ fun updateItemsJSON(value: String?) {
97
+ val next = parseHeaderItems(value)
98
+ if (items == next) return
99
+ items = next
100
+ buttons.forEach(content::removeView)
101
+ buttons.clear()
102
+ itemFrames.clear()
103
+ items.forEachIndexed { index, item ->
104
+ val button = TextView(context).apply {
105
+ text = item.title
106
+ contentDescription = item.accessibilityLabel
107
+ setTag(com.facebook.react.R.id.react_test_id, item.testID)
108
+ gravity = Gravity.CENTER
109
+ isSingleLine = true
110
+ ellipsize = TextUtils.TruncateAt.END
111
+ isClickable = true
112
+ isFocusable = true
113
+ setPadding(dp(8.0), 0, dp(8.0), 0)
114
+ setOnClickListener { onItemPress?.invoke(index, item.key) }
115
+ }
116
+ buttons.add(button)
117
+ content.addView(button)
118
+ }
119
+ visibility = if (items.isEmpty()) GONE else VISIBLE
120
+ centerSelectionWhenIdle = true
121
+ requestLayout()
122
+ }
123
+
124
+ fun updateSelectedKey(value: String) {
125
+ if (selectedKey == value) return
126
+ selectedKey = value
127
+ centerSelectionWhenIdle = true
128
+ updatePresentation()
129
+ }
130
+
131
+ fun updateStyle(
132
+ rowHeight: Double,
133
+ contentPadding: Double,
134
+ itemSpacing: Double,
135
+ fontSize: Double,
136
+ fontFamily: String?,
137
+ indicatorHeight: Double = 0.0,
138
+ indicatorBottom: Double = 0.0,
139
+ ) {
140
+ rowHeightPx = max(1, dp(rowHeight))
141
+ contentPaddingPx = max(0, dp(contentPadding))
142
+ itemSpacingPx = max(0, dp(itemSpacing))
143
+ this.fontSize = max(1.0, fontSize)
144
+ this.fontFamily = fontFamily
145
+ if (showsProgressIndicator) {
146
+ indicatorHeightPx = max(0, dp(indicatorHeight))
147
+ indicatorBottomPx = max(0, dp(indicatorBottom))
148
+ indicatorBackground.cornerRadius = indicatorHeightPx / 2f
149
+ }
150
+ applyTypeface()
151
+ requestLayout()
152
+ }
153
+
154
+ fun updateColors(
155
+ backgroundColor: Int,
156
+ activeTextColor: Int,
157
+ inactiveTextColor: Int,
158
+ selectedBackgroundColor: Int,
159
+ indicatorColor: Int,
160
+ ) {
161
+ setBackgroundColor(backgroundColor)
162
+ content.setBackgroundColor(backgroundColor)
163
+ this.activeTextColor = activeTextColor
164
+ this.inactiveTextColor = inactiveTextColor
165
+ this.selectedBackgroundColor = selectedBackgroundColor
166
+ this.indicatorColor = indicatorColor
167
+ indicatorBackground.setColor(indicatorColor)
168
+ updatePresentation()
169
+ }
170
+
171
+ fun setProgress(value: Float) {
172
+ if (!showsProgressIndicator || items.isEmpty()) return
173
+ val next = value.coerceIn(0f, (items.size - 1).toFloat())
174
+ if (kotlin.math.abs(progress - next) >= 0.001f) centerSelectionWhenIdle = true
175
+ progress = next
176
+ updatePresentation()
177
+ }
178
+
179
+ private fun applyTypeface() {
180
+ val typeface = Typeface.create(fontFamily ?: "sans-serif-medium", Typeface.NORMAL)
181
+ buttons.forEach { button ->
182
+ button.typeface = typeface
183
+ button.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize.toFloat())
184
+ }
185
+ }
186
+
187
+ private fun selectedBackground(selected: Boolean) = GradientDrawable().apply {
188
+ shape = GradientDrawable.RECTANGLE
189
+ setColor(if (selected) selectedBackgroundColor else Color.TRANSPARENT)
190
+ cornerRadius = dp(10.0).toFloat()
191
+ }
192
+
193
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
194
+ val width = MeasureSpec.getSize(widthMeasureSpec)
195
+ val height = when (MeasureSpec.getMode(heightMeasureSpec)) {
196
+ MeasureSpec.EXACTLY -> MeasureSpec.getSize(heightMeasureSpec)
197
+ else -> rowHeightPx
198
+ }
199
+ applyTypeface()
200
+ itemFrames.clear()
201
+ var x = contentPaddingPx
202
+ buttons.forEachIndexed { index, button ->
203
+ val textWidth = ceil(button.paint.measureText(button.text.toString()).toDouble()).toInt()
204
+ val buttonWidth = max(dp(44.0), textWidth + dp(if (showsProgressIndicator) 16.0 else 20.0))
205
+ val indicatorWidth = textWidth
206
+ val indicatorX = x + (buttonWidth - indicatorWidth) / 2
207
+ itemFrames.add(intArrayOf(x, buttonWidth, indicatorX, indicatorWidth))
208
+ val itemHeight = if (showsProgressIndicator) height else min(dp(32.0), height)
209
+ val top = if (showsProgressIndicator) 0 else max(0, (height - itemHeight) / 2)
210
+ button.layoutParams = FrameLayout.LayoutParams(buttonWidth, itemHeight).apply {
211
+ leftMargin = x
212
+ topMargin = top
213
+ }
214
+ x += buttonWidth
215
+ if (index < buttons.lastIndex) x += itemSpacingPx
216
+ }
217
+ val contentWidth = max(width, x + contentPaddingPx)
218
+ content.layoutParams = LayoutParams(contentWidth, height)
219
+ content.measure(
220
+ MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY),
221
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
222
+ )
223
+ setMeasuredDimension(width, height)
224
+ }
225
+
226
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
227
+ super.onLayout(changed, left, top, right, bottom)
228
+ updatePresentation()
229
+ }
230
+
231
+ override fun onTouchEvent(event: MotionEvent): Boolean {
232
+ when (event.actionMasked) {
233
+ MotionEvent.ACTION_DOWN -> isTouchDragging = true
234
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
235
+ isTouchDragging = false
236
+ post(::updatePresentation)
237
+ }
238
+ }
239
+ return super.onTouchEvent(event)
240
+ }
241
+
242
+ private fun updatePresentation() {
243
+ if (buttons.isEmpty() || itemFrames.size != buttons.size) return
244
+ val selectedIndex: Int
245
+ if (showsProgressIndicator) {
246
+ val clamped = progress.coerceIn(0f, buttons.lastIndex.toFloat())
247
+ val lower = floor(clamped).toInt()
248
+ val upper = min(lower + 1, buttons.lastIndex)
249
+ val fraction = clamped - lower
250
+ val from = itemFrames[lower]
251
+ val to = itemFrames[upper]
252
+ val indicatorX = (from[2] + (to[2] - from[2]) * fraction).roundToInt()
253
+ val indicatorWidth = (from[3] + (to[3] - from[3]) * fraction).roundToInt()
254
+ val indicatorTop = max(0, measuredHeight - indicatorBottomPx - indicatorHeightPx)
255
+ indicator.layout(
256
+ indicatorX,
257
+ indicatorTop,
258
+ indicatorX + indicatorWidth,
259
+ indicatorTop + indicatorHeightPx,
260
+ )
261
+ indicator.visibility = if (indicatorHeightPx > 0) VISIBLE else GONE
262
+ selectedIndex = clamped.roundToInt()
263
+ buttons.forEachIndexed { index, button ->
264
+ val emphasis = 1f - min(1f, kotlin.math.abs(clamped - index))
265
+ button.setTextColor(ColorUtils.blendARGB(inactiveTextColor, activeTextColor, emphasis))
266
+ button.background = null
267
+ button.isSelected = index == selectedIndex
268
+ }
269
+ } else {
270
+ selectedIndex = items.indexOfFirst { item -> item.key == selectedKey }
271
+ indicator.visibility = GONE
272
+ buttons.forEachIndexed { index, button ->
273
+ val selected = index == selectedIndex
274
+ button.setTextColor(if (selected) activeTextColor else inactiveTextColor)
275
+ button.background = selectedBackground(selected)
276
+ button.isSelected = selected
277
+ }
278
+ }
279
+
280
+ if (centerSelectionWhenIdle && !isTouchDragging && width > 0 && selectedIndex in itemFrames.indices) {
281
+ val frame = itemFrames[selectedIndex]
282
+ val desired = (frame[0] + frame[1] / 2 - width / 2)
283
+ .coerceIn(0, max(0, content.measuredWidth - width))
284
+ scrollTo(desired, 0)
285
+ centerSelectionWhenIdle = false
286
+ }
287
+ }
288
+ }
289
+
290
+ internal class CollapsiblePagerNativeTabBarView(context: Context) : FrameLayout(context) {
291
+ private val row = CollapsiblePagerHorizontalItemsView(context, true)
292
+ private var heightPx = 0
293
+
294
+ var onItemPress: ((Int, String) -> Unit)?
295
+ get() = row.onItemPress
296
+ set(value) {
297
+ row.onItemPress = value
298
+ }
299
+
300
+ init {
301
+ addView(row, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
302
+ visibility = GONE
303
+ }
304
+
305
+ fun updateItemsJSON(value: String?) {
306
+ row.updateItemsJSON(value)
307
+ visibility = row.visibility
308
+ }
309
+
310
+ fun updateStyle(
311
+ height: Double,
312
+ contentPadding: Double,
313
+ itemSpacing: Double,
314
+ fontSize: Double,
315
+ fontFamily: String?,
316
+ indicatorHeight: Double,
317
+ indicatorBottom: Double,
318
+ ) {
319
+ heightPx = max(1, (height * resources.displayMetrics.density).roundToInt())
320
+ row.updateStyle(
321
+ height,
322
+ contentPadding,
323
+ itemSpacing,
324
+ fontSize,
325
+ fontFamily,
326
+ indicatorHeight,
327
+ indicatorBottom,
328
+ )
329
+ requestLayout()
330
+ }
331
+
332
+ fun updateColors(
333
+ backgroundColor: Int,
334
+ activeTextColor: Int,
335
+ inactiveTextColor: Int,
336
+ indicatorColor: Int,
337
+ ) = row.updateColors(
338
+ backgroundColor,
339
+ activeTextColor,
340
+ inactiveTextColor,
341
+ Color.TRANSPARENT,
342
+ indicatorColor,
343
+ )
344
+
345
+ fun setProgress(progress: Float) = row.setProgress(progress)
346
+
347
+ fun preferredHeightPx(): Int = heightPx
348
+ }
349
+
350
+ internal class CollapsiblePagerNativeSubHeaderView(context: Context) : ViewGroup(context) {
351
+ private val density = resources.displayMetrics.density
352
+ private val tabs = CollapsiblePagerHorizontalItemsView(context, false)
353
+ private val columns = FrameLayout(context)
354
+ private val leading = TextView(context)
355
+ private val middle = TextView(context)
356
+ private val trailing = TextView(context)
357
+ private var configuredHeightPx = dp(74.0)
358
+ private var tabsHeightPx = dp(42.0)
359
+ private var contentPaddingPx = dp(20.0)
360
+ private var itemSpacing = 8.0
361
+ private var itemFontSize = 14.0
362
+ private var columnFontSize = 12.0
363
+ private var trailingColumnWidthPx = dp(80.0)
364
+ private var columnGapPx = dp(8.0)
365
+ private var backgroundColorValue = Color.TRANSPARENT
366
+ private var activeTextColor = Color.BLACK
367
+ private var inactiveTextColor = Color.GRAY
368
+ private var selectedBackgroundColor = Color.TRANSPARENT
369
+ private var fontFamily: String? = null
370
+
371
+ var onItemPress: ((Int, String) -> Unit)?
372
+ get() = tabs.onItemPress
373
+ set(value) {
374
+ tabs.onItemPress = value
375
+ }
376
+
377
+ init {
378
+ addView(tabs)
379
+ addView(columns)
380
+ columns.addView(leading)
381
+ columns.addView(middle)
382
+ columns.addView(trailing)
383
+ listOf(leading, middle, trailing).forEach { label ->
384
+ label.gravity = Gravity.CENTER_VERTICAL
385
+ label.isSingleLine = true
386
+ label.ellipsize = TextUtils.TruncateAt.END
387
+ }
388
+ visibility = GONE
389
+ }
390
+
391
+ private fun dp(value: Double): Int = (value * density).roundToInt()
392
+
393
+ fun updateConfigJSON(value: String?) {
394
+ val config = runCatching { JSONObject(value ?: "{}") }.getOrDefault(JSONObject())
395
+ val style = config.optJSONObject("style") ?: JSONObject()
396
+ val columnsConfig = config.optJSONObject("columns") ?: JSONObject()
397
+ val itemArray = config.optJSONArray("items") ?: JSONArray()
398
+ tabs.updateItemsJSON(itemArray.toString())
399
+ tabs.updateSelectedKey(config.optString("selectedKey"))
400
+ configuredHeightPx = max(1, dp(style.optDouble("height", 74.0)))
401
+ tabsHeightPx = max(0, dp(style.optDouble("tabsHeight", 42.0)))
402
+ contentPaddingPx = max(0, dp(style.optDouble("contentPaddingHorizontal", 20.0)))
403
+ itemSpacing = max(0.0, style.optDouble("itemSpacing", 8.0))
404
+ itemFontSize = max(1.0, style.optDouble("fontSize", 14.0))
405
+ columnFontSize = max(1.0, style.optDouble("columnFontSize", 12.0))
406
+ trailingColumnWidthPx = max(0, dp(style.optDouble("trailingColumnWidth", 80.0)))
407
+ columnGapPx = max(0, dp(style.optDouble("columnGap", 8.0)))
408
+ leading.text = columnsConfig.optString("leading")
409
+ middle.text = columnsConfig.optString("middle")
410
+ trailing.text = columnsConfig.optString("trailing")
411
+ visibility = if (itemArray.length() == 0) GONE else VISIBLE
412
+ applyStyle()
413
+ requestLayout()
414
+ }
415
+
416
+ fun updateColors(
417
+ backgroundColor: Int,
418
+ activeTextColor: Int,
419
+ inactiveTextColor: Int,
420
+ selectedBackgroundColor: Int,
421
+ fontFamily: String?,
422
+ ) {
423
+ backgroundColorValue = backgroundColor
424
+ this.activeTextColor = activeTextColor
425
+ this.inactiveTextColor = inactiveTextColor
426
+ this.selectedBackgroundColor = selectedBackgroundColor
427
+ this.fontFamily = fontFamily
428
+ applyStyle()
429
+ }
430
+
431
+ private fun applyStyle() {
432
+ setBackgroundColor(backgroundColorValue)
433
+ columns.setBackgroundColor(backgroundColorValue)
434
+ tabs.updateStyle(
435
+ tabsHeightPx / density.toDouble(),
436
+ contentPaddingPx / density.toDouble(),
437
+ itemSpacing,
438
+ itemFontSize,
439
+ fontFamily,
440
+ )
441
+ tabs.updateColors(
442
+ backgroundColorValue,
443
+ activeTextColor,
444
+ inactiveTextColor,
445
+ selectedBackgroundColor,
446
+ activeTextColor,
447
+ )
448
+ val typeface = Typeface.create(fontFamily ?: "sans-serif-medium", Typeface.NORMAL)
449
+ listOf(leading, middle, trailing).forEach { label ->
450
+ label.typeface = typeface
451
+ label.setTextSize(TypedValue.COMPLEX_UNIT_SP, columnFontSize.toFloat())
452
+ label.setTextColor(inactiveTextColor)
453
+ }
454
+ }
455
+
456
+ fun preferredHeightPx(): Int = configuredHeightPx
457
+
458
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
459
+ val width = MeasureSpec.getSize(widthMeasureSpec)
460
+ val height = MeasureSpec.getSize(heightMeasureSpec)
461
+ val actualTabsHeight = min(height, tabsHeightPx)
462
+ tabs.measure(
463
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
464
+ MeasureSpec.makeMeasureSpec(actualTabsHeight, MeasureSpec.EXACTLY),
465
+ )
466
+ val columnsHeight = max(0, height - actualTabsHeight)
467
+ columns.measure(
468
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
469
+ MeasureSpec.makeMeasureSpec(columnsHeight, MeasureSpec.EXACTLY),
470
+ )
471
+ setMeasuredDimension(width, height)
472
+ }
473
+
474
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
475
+ val width = right - left
476
+ val height = bottom - top
477
+ val actualTabsHeight = min(height, tabsHeightPx)
478
+ tabs.layout(0, 0, width, actualTabsHeight)
479
+ columns.layout(0, actualTabsHeight, width, height)
480
+ val columnsHeight = height - actualTabsHeight
481
+ val half = width / 2
482
+ val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
483
+ if (!isRtl) {
484
+ leading.gravity = Gravity.START or Gravity.CENTER_VERTICAL
485
+ middle.gravity = Gravity.END or Gravity.CENTER_VERTICAL
486
+ trailing.gravity = Gravity.END or Gravity.CENTER_VERTICAL
487
+ val trailingX = width - contentPaddingPx - trailingColumnWidthPx
488
+ leading.layout(contentPaddingPx, 0, half, columnsHeight)
489
+ middle.layout(half, 0, max(half, trailingX - columnGapPx), columnsHeight)
490
+ trailing.layout(trailingX, 0, width - contentPaddingPx, columnsHeight)
491
+ } else {
492
+ leading.gravity = Gravity.END or Gravity.CENTER_VERTICAL
493
+ middle.gravity = Gravity.START or Gravity.CENTER_VERTICAL
494
+ trailing.gravity = Gravity.START or Gravity.CENTER_VERTICAL
495
+ leading.layout(half, 0, width - contentPaddingPx, columnsHeight)
496
+ middle.layout(
497
+ contentPaddingPx + trailingColumnWidthPx + columnGapPx,
498
+ 0,
499
+ half,
500
+ columnsHeight,
501
+ )
502
+ trailing.layout(contentPaddingPx, 0, contentPaddingPx + trailingColumnWidthPx, columnsHeight)
503
+ }
504
+ }
505
+ }