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