@onekeyfe/react-native-pager-view 3.0.119 → 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 +109 -20
- package/android/src/main/java/com/reactnativepagerview/event/NativeHeaderPressEvent.kt +31 -0
- package/lib/module/CollapsiblePagerView.js +3 -3
- package/lib/typescript/src/CollapsiblePagerView.d.ts +4 -4
- package/package.json +3 -2
- package/src/CollapsiblePagerView.tsx +12 -7
package/android/build.gradle
CHANGED
|
@@ -226,6 +226,7 @@ dependencies {
|
|
|
226
226
|
implementation "com.facebook.react:react-android"
|
|
227
227
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
228
228
|
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
|
229
|
+
implementation project(":onekeyfe_react-native-native-logger")
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
if (isNewArchitectureEnabled()) {
|
|
@@ -2,9 +2,13 @@ package com.reactnativepagerview
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Rect
|
|
5
|
+
import android.os.SystemClock
|
|
6
|
+
import android.view.MotionEvent
|
|
5
7
|
import android.view.View
|
|
8
|
+
import android.view.ViewConfiguration
|
|
6
9
|
import android.view.ViewGroup
|
|
7
10
|
import android.view.ViewTreeObserver
|
|
11
|
+
import android.widget.HorizontalScrollView
|
|
8
12
|
// OneKey patch: FrameLayout is inherited through NestedScrollableHost.
|
|
9
13
|
// import android.widget.FrameLayout
|
|
10
14
|
import androidx.core.view.NestedScrollingParent3
|
|
@@ -13,6 +17,8 @@ import androidx.core.view.ViewCompat
|
|
|
13
17
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
14
18
|
import androidx.recyclerview.widget.RecyclerView
|
|
15
19
|
import androidx.viewpager2.widget.ViewPager2
|
|
20
|
+
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
21
|
+
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
16
22
|
import java.util.WeakHashMap
|
|
17
23
|
import kotlin.math.max
|
|
18
24
|
import kotlin.math.min
|
|
@@ -72,6 +78,13 @@ internal class CollapsiblePagerAdapter : RecyclerView.Adapter<ViewPagerViewHolde
|
|
|
72
78
|
// OneKey patch: Match the standard pager's nested horizontal gesture host.
|
|
73
79
|
// Original: class CollapsiblePagerHost(context: Context) : FrameLayout(context), NestedScrollingParent3 {
|
|
74
80
|
class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), NestedScrollingParent3 {
|
|
81
|
+
private enum class HeaderGestureOwner {
|
|
82
|
+
NONE,
|
|
83
|
+
LIST,
|
|
84
|
+
HORIZONTAL_CHILD,
|
|
85
|
+
HEADER_GUARD,
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
private data class RecyclerPadding(
|
|
76
89
|
var left: Int,
|
|
77
90
|
var top: Int,
|
|
@@ -83,6 +96,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
83
96
|
|
|
84
97
|
val pager = ViewPager2(context)
|
|
85
98
|
internal val adapter = CollapsiblePagerAdapter()
|
|
99
|
+
private val nativeTabBarView = CollapsiblePagerNativeTabBarView(context)
|
|
100
|
+
private val nativeSubHeaderView = CollapsiblePagerNativeSubHeaderView(context)
|
|
86
101
|
private val logicalChildren = ArrayList<View>()
|
|
87
102
|
private val nestedScrollingParentHelper = NestedScrollingParentHelper(this)
|
|
88
103
|
private val originalRecyclerPadding = WeakHashMap<RecyclerView, RecyclerPadding>()
|
|
@@ -91,8 +106,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
91
106
|
private var observedRecyclerView: RecyclerView? = null
|
|
92
107
|
private var observedScrollListener: RecyclerView.OnScrollListener? = null
|
|
93
108
|
private var attachmentGeneration = 0
|
|
109
|
+
private val touchSlopPx = ViewConfiguration.get(context).scaledTouchSlop
|
|
110
|
+
private val hostIdentity = System.identityHashCode(this)
|
|
94
111
|
private var headerView: View? = null
|
|
95
112
|
private var stickyHeaderView: View? = null
|
|
113
|
+
private var headerTouchActive = false
|
|
114
|
+
private var headerTouchRegion = "none"
|
|
115
|
+
private var headerGestureOwner = HeaderGestureOwner.NONE
|
|
116
|
+
private var headerDownX = 0f
|
|
117
|
+
private var headerDownY = 0f
|
|
118
|
+
private var headerDownEvent: MotionEvent? = null
|
|
119
|
+
private var headerHasHorizontalChild = false
|
|
120
|
+
private var forwardedRecycler: RecyclerView? = null
|
|
121
|
+
private var nativeGestureStarted = false
|
|
122
|
+
private var pressCancelled = false
|
|
96
123
|
private val pageContentLayoutListener = ViewTreeObserver.OnPreDrawListener {
|
|
97
124
|
if (width > 0 && height > 0 && isShown) layoutPagerIfRequested()
|
|
98
125
|
// Fabric can mount a retained list without another Android layout pass.
|
|
@@ -128,6 +155,22 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
128
155
|
var pageKeys: List<String> = emptyList()
|
|
129
156
|
var retainedPages: String = "[]"
|
|
130
157
|
var onHeaderOffsetChanged: (() -> Unit)? = null
|
|
158
|
+
var nativeSmoothHeaderScrollEnabled = false
|
|
159
|
+
var onNativeTabPress: ((Int, String) -> Unit)? = null
|
|
160
|
+
var onNativeSubHeaderPress: ((Int, String) -> Unit)? = null
|
|
161
|
+
|
|
162
|
+
var nativeTabBarHeight = 44.0
|
|
163
|
+
var nativeTabBarContentPaddingHorizontal = 20.0
|
|
164
|
+
var nativeTabBarItemSpacing = 8.0
|
|
165
|
+
var nativeTabBarFontSize = 16.0
|
|
166
|
+
var nativeTabBarFontFamily: String? = null
|
|
167
|
+
var nativeTabBarBackgroundColor = android.graphics.Color.TRANSPARENT
|
|
168
|
+
var nativeTabBarActiveTextColor = android.graphics.Color.BLACK
|
|
169
|
+
var nativeTabBarInactiveTextColor = android.graphics.Color.GRAY
|
|
170
|
+
var nativeTabBarIndicatorColor = android.graphics.Color.BLACK
|
|
171
|
+
var nativeTabBarIndicatorHeight = 2.0
|
|
172
|
+
var nativeTabBarIndicatorBottom = 0.0
|
|
173
|
+
var nativeSubHeaderSelectedBackgroundColor = android.graphics.Color.TRANSPARENT
|
|
131
174
|
|
|
132
175
|
init {
|
|
133
176
|
isSaveEnabled = false
|
|
@@ -139,6 +182,88 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
139
182
|
pager,
|
|
140
183
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
|
|
141
184
|
)
|
|
185
|
+
nativeTabBarView.onItemPress = { index, key -> onNativeTabPress?.invoke(index, key) }
|
|
186
|
+
nativeSubHeaderView.onItemPress = { index, key -> onNativeSubHeaderPress?.invoke(index, key) }
|
|
187
|
+
super.addView(nativeTabBarView)
|
|
188
|
+
super.addView(nativeSubHeaderView)
|
|
189
|
+
applyNativeHeaderStyle()
|
|
190
|
+
log("host-init generation=$attachmentGeneration")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private fun log(message: String) {
|
|
194
|
+
OneKeyLog.debug("CollapsiblePager", "host=$hostIdentity $message")
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private fun outerPagerIndex(): Int {
|
|
198
|
+
var ancestor = parent as? View
|
|
199
|
+
while (ancestor != null) {
|
|
200
|
+
if (ancestor is ViewPager2) return ancestor.currentItem
|
|
201
|
+
ancestor = ancestor.parent as? View
|
|
202
|
+
}
|
|
203
|
+
return -1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fun updateNativeTabBarItems(value: String?) {
|
|
207
|
+
nativeTabBarView.updateItemsJSON(value)
|
|
208
|
+
bringNativeHeadersToFront()
|
|
209
|
+
updateHeaderLayout()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fun updateNativeSubHeader(value: String?) {
|
|
213
|
+
nativeSubHeaderView.updateConfigJSON(value)
|
|
214
|
+
applyNativeHeaderStyle()
|
|
215
|
+
bringNativeHeadersToFront()
|
|
216
|
+
updateHeaderLayout()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fun updateNativeTabProgress(position: Int, offset: Float) {
|
|
220
|
+
nativeTabBarView.setProgress(position + offset)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fun logPagerState(reason: String) {
|
|
224
|
+
log(
|
|
225
|
+
"pager-state reason=$reason inner=$selectedPage outer=${outerPagerIndex()} " +
|
|
226
|
+
"nativePages=${adapter.itemCount} attachedPages=${attachedPageCount()} " +
|
|
227
|
+
"headerOffset=$headerOffsetPx retained=$retainedPages",
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fun setPagerLayoutDirection(layoutDirection: Int) {
|
|
232
|
+
pager.layoutDirection = layoutDirection
|
|
233
|
+
nativeTabBarView.layoutDirection = layoutDirection
|
|
234
|
+
nativeSubHeaderView.layoutDirection = layoutDirection
|
|
235
|
+
nativeSubHeaderView.requestLayout()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fun applyNativeHeaderStyle() {
|
|
239
|
+
nativeTabBarView.updateStyle(
|
|
240
|
+
nativeTabBarHeight,
|
|
241
|
+
nativeTabBarContentPaddingHorizontal,
|
|
242
|
+
nativeTabBarItemSpacing,
|
|
243
|
+
nativeTabBarFontSize,
|
|
244
|
+
nativeTabBarFontFamily,
|
|
245
|
+
nativeTabBarIndicatorHeight,
|
|
246
|
+
nativeTabBarIndicatorBottom,
|
|
247
|
+
)
|
|
248
|
+
nativeTabBarView.updateColors(
|
|
249
|
+
nativeTabBarBackgroundColor,
|
|
250
|
+
nativeTabBarActiveTextColor,
|
|
251
|
+
nativeTabBarInactiveTextColor,
|
|
252
|
+
nativeTabBarIndicatorColor,
|
|
253
|
+
)
|
|
254
|
+
nativeSubHeaderView.updateColors(
|
|
255
|
+
nativeTabBarBackgroundColor,
|
|
256
|
+
nativeTabBarActiveTextColor,
|
|
257
|
+
nativeTabBarInactiveTextColor,
|
|
258
|
+
nativeSubHeaderSelectedBackgroundColor,
|
|
259
|
+
nativeTabBarFontFamily,
|
|
260
|
+
)
|
|
261
|
+
updateHeaderLayout()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private fun bringNativeHeadersToFront() {
|
|
265
|
+
nativeTabBarView.bringToFront()
|
|
266
|
+
nativeSubHeaderView.bringToFront()
|
|
142
267
|
}
|
|
143
268
|
|
|
144
269
|
fun addReactChild(child: View, index: Int) {
|
|
@@ -160,7 +285,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
160
285
|
applyPendingInitialPage()
|
|
161
286
|
headerView?.bringToFront()
|
|
162
287
|
stickyHeaderView?.bringToFront()
|
|
288
|
+
bringNativeHeadersToFront()
|
|
163
289
|
requestLayout()
|
|
290
|
+
if (safeIndex >= PAGE_SLOT_OFFSET) {
|
|
291
|
+
log("page-attach slot=${safeIndex - PAGE_SLOT_OFFSET} nativePages=${adapter.itemCount}")
|
|
292
|
+
}
|
|
164
293
|
}
|
|
165
294
|
|
|
166
295
|
fun removeReactChild(child: View) {
|
|
@@ -179,6 +308,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
179
308
|
else -> {
|
|
180
309
|
findVerticalRecyclerView(child)?.let(::restoreRecyclerInsets)
|
|
181
310
|
adapter.removePage(child)
|
|
311
|
+
log("page-detach slot=${index - PAGE_SLOT_OFFSET} nativePages=${adapter.itemCount}")
|
|
182
312
|
}
|
|
183
313
|
}
|
|
184
314
|
}
|
|
@@ -195,6 +325,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
195
325
|
logicalChildren.clear()
|
|
196
326
|
adapter.clear()
|
|
197
327
|
hasAppliedInitialPage = false
|
|
328
|
+
log("pages-clear")
|
|
198
329
|
}
|
|
199
330
|
|
|
200
331
|
fun reactChildCount() = logicalChildren.size
|
|
@@ -285,6 +416,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
285
416
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
286
417
|
MeasureSpec.makeMeasureSpec(stickyHeaderHeightPx, MeasureSpec.EXACTLY),
|
|
287
418
|
)
|
|
419
|
+
val nativeTabHeight = if (nativeTabBarView.visibility == View.VISIBLE) {
|
|
420
|
+
nativeTabBarView.preferredHeightPx()
|
|
421
|
+
} else 0
|
|
422
|
+
nativeTabBarView.measure(
|
|
423
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
424
|
+
MeasureSpec.makeMeasureSpec(nativeTabHeight, MeasureSpec.EXACTLY),
|
|
425
|
+
)
|
|
426
|
+
val nativeSubHeaderHeight = if (nativeSubHeaderView.visibility == View.VISIBLE) {
|
|
427
|
+
nativeSubHeaderView.preferredHeightPx()
|
|
428
|
+
} else 0
|
|
429
|
+
nativeSubHeaderView.measure(
|
|
430
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
431
|
+
MeasureSpec.makeMeasureSpec(nativeSubHeaderHeight, MeasureSpec.EXACTLY),
|
|
432
|
+
)
|
|
288
433
|
setMeasuredDimension(width, height)
|
|
289
434
|
}
|
|
290
435
|
|
|
@@ -302,6 +447,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
302
447
|
width,
|
|
303
448
|
headerHeightPx + stickyHeaderHeightPx,
|
|
304
449
|
)
|
|
450
|
+
val nativeTabHeight = nativeTabBarView.measuredHeight
|
|
451
|
+
nativeTabBarView.layout(0, headerHeightPx, width, headerHeightPx + nativeTabHeight)
|
|
452
|
+
nativeSubHeaderView.layout(
|
|
453
|
+
0,
|
|
454
|
+
headerHeightPx + nativeTabHeight,
|
|
455
|
+
width,
|
|
456
|
+
headerHeightPx + nativeTabHeight + nativeSubHeaderView.measuredHeight,
|
|
457
|
+
)
|
|
305
458
|
applyHeaderOffset()
|
|
306
459
|
postForCurrentAttachment {
|
|
307
460
|
prepareAdjacentPages()
|
|
@@ -333,6 +486,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
333
486
|
pager.translationY = -offset
|
|
334
487
|
headerView?.translationY = -offset
|
|
335
488
|
stickyHeaderView?.translationY = -offset
|
|
489
|
+
nativeTabBarView.translationY = -offset
|
|
490
|
+
nativeSubHeaderView.translationY = -offset
|
|
336
491
|
}
|
|
337
492
|
|
|
338
493
|
private fun pageKey(index: Int): String = pageKeys.getOrNull(index) ?: "page-$index"
|
|
@@ -408,7 +563,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
408
563
|
}
|
|
409
564
|
observedScrollListener = listener
|
|
410
565
|
recycler.addOnScrollListener(listener)
|
|
411
|
-
|
|
566
|
+
log(
|
|
567
|
+
"list-observer-attach inner=$selectedPage key=${currentPageKey()} " +
|
|
568
|
+
"recycler=${System.identityHashCode(recycler)}",
|
|
569
|
+
)
|
|
412
570
|
}
|
|
413
571
|
|
|
414
572
|
private fun detachRecyclerObserver() {
|
|
@@ -416,6 +574,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
416
574
|
val recycler = observedRecyclerView
|
|
417
575
|
val listener = observedScrollListener
|
|
418
576
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
577
|
+
if (recycler != null) {
|
|
578
|
+
log(
|
|
579
|
+
"list-observer-detach inner=$selectedPage key=${currentPageKey()} " +
|
|
580
|
+
"recycler=${System.identityHashCode(recycler)}",
|
|
581
|
+
)
|
|
582
|
+
}
|
|
419
583
|
observedRecyclerView = null
|
|
420
584
|
observedScrollListener = null
|
|
421
585
|
}
|
|
@@ -513,6 +677,195 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
513
677
|
recycler.setPadding(original.left, original.top, original.right, original.bottom)
|
|
514
678
|
}
|
|
515
679
|
|
|
680
|
+
private fun headerRegionAt(y: Float): String? {
|
|
681
|
+
val headerBottom = headerHeightPx - headerOffsetPx
|
|
682
|
+
val stickyBottom = headerHeightPx + stickyHeaderHeightPx - headerOffsetPx
|
|
683
|
+
return when {
|
|
684
|
+
y < 0 || y >= stickyBottom -> null
|
|
685
|
+
y < headerBottom -> "header"
|
|
686
|
+
y < headerBottom + nativeTabBarView.measuredHeight &&
|
|
687
|
+
nativeTabBarView.visibility == View.VISIBLE -> "primary-tab"
|
|
688
|
+
y < headerBottom + nativeTabBarView.measuredHeight + nativeSubHeaderView.measuredHeight &&
|
|
689
|
+
nativeSubHeaderView.visibility == View.VISIBLE -> "secondary-header"
|
|
690
|
+
else -> "sticky-controls"
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private fun deepestChildAt(group: ViewGroup, x: Float, y: Float): View {
|
|
695
|
+
for (index in group.childCount - 1 downTo 0) {
|
|
696
|
+
val child = group.getChildAt(index)
|
|
697
|
+
if (child.visibility != View.VISIBLE || child.alpha <= 0f) continue
|
|
698
|
+
val localX = x + group.scrollX - child.left - child.translationX
|
|
699
|
+
val localY = y + group.scrollY - child.top - child.translationY
|
|
700
|
+
if (localX < 0 || localY < 0 || localX >= child.width || localY >= child.height) continue
|
|
701
|
+
return if (child is ViewGroup) deepestChildAt(child, localX, localY) else child
|
|
702
|
+
}
|
|
703
|
+
return group
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private fun hasHorizontalScrollOwner(x: Float, y: Float): Boolean {
|
|
707
|
+
var target: View? = deepestChildAt(this, x, y)
|
|
708
|
+
while (target != null && target !== this) {
|
|
709
|
+
if (
|
|
710
|
+
target is HorizontalScrollView ||
|
|
711
|
+
target.canScrollHorizontally(-1) ||
|
|
712
|
+
target.canScrollHorizontally(1) ||
|
|
713
|
+
(target is RecyclerView &&
|
|
714
|
+
(target.layoutManager as? LinearLayoutManager)?.orientation == RecyclerView.HORIZONTAL)
|
|
715
|
+
) {
|
|
716
|
+
return true
|
|
717
|
+
}
|
|
718
|
+
target = target.parent as? View
|
|
719
|
+
}
|
|
720
|
+
return false
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private fun cancelHeaderPress(event: MotionEvent, owner: String) {
|
|
724
|
+
if (pressCancelled) return
|
|
725
|
+
pressCancelled = true
|
|
726
|
+
val cancel = MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_CANCEL }
|
|
727
|
+
super.dispatchTouchEvent(cancel)
|
|
728
|
+
cancel.recycle()
|
|
729
|
+
if (!nativeGestureStarted) {
|
|
730
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, event)
|
|
731
|
+
nativeGestureStarted = true
|
|
732
|
+
}
|
|
733
|
+
log(
|
|
734
|
+
"press-cancel owner=$owner region=$headerTouchRegion " +
|
|
735
|
+
"inner=$selectedPage outer=${outerPagerIndex()}",
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private fun dispatchToRecycler(recycler: RecyclerView, event: MotionEvent): Boolean {
|
|
740
|
+
val hostLocation = IntArray(2)
|
|
741
|
+
val recyclerLocation = IntArray(2)
|
|
742
|
+
getLocationOnScreen(hostLocation)
|
|
743
|
+
recycler.getLocationOnScreen(recyclerLocation)
|
|
744
|
+
val copy = MotionEvent.obtain(event)
|
|
745
|
+
copy.offsetLocation(
|
|
746
|
+
(hostLocation[0] - recyclerLocation[0]).toFloat(),
|
|
747
|
+
(hostLocation[1] - recyclerLocation[1]).toFloat(),
|
|
748
|
+
)
|
|
749
|
+
val handled = recycler.dispatchTouchEvent(copy)
|
|
750
|
+
copy.recycle()
|
|
751
|
+
return handled
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private fun beginForwardingToRecycler(event: MotionEvent): Boolean {
|
|
755
|
+
val recycler = recyclerViewForPage(selectedPage) ?: return false
|
|
756
|
+
forwardedRecycler = recycler
|
|
757
|
+
headerDownEvent?.let { down -> dispatchToRecycler(recycler, down) }
|
|
758
|
+
return dispatchToRecycler(recycler, event)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private fun finishHeaderGesture(event: MotionEvent) {
|
|
762
|
+
val owner = headerGestureOwner.name.lowercase()
|
|
763
|
+
if (nativeGestureStarted) {
|
|
764
|
+
NativeGestureUtil.notifyNativeGestureEnded(this, event)
|
|
765
|
+
nativeGestureStarted = false
|
|
766
|
+
}
|
|
767
|
+
log(
|
|
768
|
+
"gesture-end action=${event.actionMasked} owner=$owner region=$headerTouchRegion " +
|
|
769
|
+
"inner=$selectedPage outer=${outerPagerIndex()} durationMs=" +
|
|
770
|
+
"${SystemClock.uptimeMillis() - event.downTime}",
|
|
771
|
+
)
|
|
772
|
+
headerDownEvent?.recycle()
|
|
773
|
+
headerDownEvent = null
|
|
774
|
+
headerTouchActive = false
|
|
775
|
+
headerTouchRegion = "none"
|
|
776
|
+
headerGestureOwner = HeaderGestureOwner.NONE
|
|
777
|
+
headerHasHorizontalChild = false
|
|
778
|
+
forwardedRecycler = null
|
|
779
|
+
pressCancelled = false
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
|
|
783
|
+
val event = e
|
|
784
|
+
if (!nativeSmoothHeaderScrollEnabled) return super.dispatchTouchEvent(event)
|
|
785
|
+
|
|
786
|
+
when (event.actionMasked) {
|
|
787
|
+
MotionEvent.ACTION_DOWN -> {
|
|
788
|
+
headerDownEvent?.recycle()
|
|
789
|
+
headerDownEvent = null
|
|
790
|
+
headerGestureOwner = HeaderGestureOwner.NONE
|
|
791
|
+
forwardedRecycler = null
|
|
792
|
+
pressCancelled = false
|
|
793
|
+
nativeGestureStarted = false
|
|
794
|
+
headerTouchRegion = headerRegionAt(event.y) ?: "none"
|
|
795
|
+
headerTouchActive = headerTouchRegion != "none"
|
|
796
|
+
if (!headerTouchActive) return super.dispatchTouchEvent(event)
|
|
797
|
+
headerDownX = event.x
|
|
798
|
+
headerDownY = event.y
|
|
799
|
+
headerDownEvent = MotionEvent.obtain(event)
|
|
800
|
+
headerHasHorizontalChild = hasHorizontalScrollOwner(event.x, event.y)
|
|
801
|
+
parent.requestDisallowInterceptTouchEvent(true)
|
|
802
|
+
log(
|
|
803
|
+
"gesture-begin region=$headerTouchRegion owner=pending " +
|
|
804
|
+
"horizontalChild=$headerHasHorizontalChild inner=$selectedPage " +
|
|
805
|
+
"outer=${outerPagerIndex()} headerOffset=$headerOffsetPx",
|
|
806
|
+
)
|
|
807
|
+
return super.dispatchTouchEvent(event)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
MotionEvent.ACTION_MOVE -> {
|
|
811
|
+
if (!headerTouchActive) return super.dispatchTouchEvent(event)
|
|
812
|
+
if (headerGestureOwner == HeaderGestureOwner.NONE) {
|
|
813
|
+
val dx = event.x - headerDownX
|
|
814
|
+
val dy = event.y - headerDownY
|
|
815
|
+
if (kotlin.math.abs(dx) <= touchSlopPx && kotlin.math.abs(dy) <= touchSlopPx) {
|
|
816
|
+
return super.dispatchTouchEvent(event)
|
|
817
|
+
}
|
|
818
|
+
headerGestureOwner = when {
|
|
819
|
+
kotlin.math.abs(dy) > kotlin.math.abs(dx) -> HeaderGestureOwner.LIST
|
|
820
|
+
headerHasHorizontalChild -> HeaderGestureOwner.HORIZONTAL_CHILD
|
|
821
|
+
else -> HeaderGestureOwner.HEADER_GUARD
|
|
822
|
+
}
|
|
823
|
+
log(
|
|
824
|
+
"direction-lock owner=${headerGestureOwner.name.lowercase()} " +
|
|
825
|
+
"region=$headerTouchRegion dx=${dx.roundToInt()} dy=${dy.roundToInt()} " +
|
|
826
|
+
"inner=$selectedPage outer=${outerPagerIndex()}",
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
parent.requestDisallowInterceptTouchEvent(true)
|
|
830
|
+
return when (headerGestureOwner) {
|
|
831
|
+
HeaderGestureOwner.LIST -> {
|
|
832
|
+
cancelHeaderPress(event, "list")
|
|
833
|
+
val recycler = forwardedRecycler
|
|
834
|
+
if (recycler != null) {
|
|
835
|
+
dispatchToRecycler(recycler, event)
|
|
836
|
+
} else if (beginForwardingToRecycler(event)) {
|
|
837
|
+
true
|
|
838
|
+
} else {
|
|
839
|
+
headerGestureOwner = HeaderGestureOwner.HEADER_GUARD
|
|
840
|
+
log("gesture-list-unavailable region=$headerTouchRegion inner=$selectedPage")
|
|
841
|
+
true
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
HeaderGestureOwner.HORIZONTAL_CHILD -> super.dispatchTouchEvent(event)
|
|
845
|
+
HeaderGestureOwner.HEADER_GUARD -> {
|
|
846
|
+
cancelHeaderPress(event, "header-guard")
|
|
847
|
+
true
|
|
848
|
+
}
|
|
849
|
+
HeaderGestureOwner.NONE -> super.dispatchTouchEvent(event)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
854
|
+
if (!headerTouchActive) return super.dispatchTouchEvent(event)
|
|
855
|
+
val handled = when (headerGestureOwner) {
|
|
856
|
+
HeaderGestureOwner.LIST -> forwardedRecycler?.let { recycler ->
|
|
857
|
+
dispatchToRecycler(recycler, event)
|
|
858
|
+
} ?: true
|
|
859
|
+
HeaderGestureOwner.HEADER_GUARD -> true
|
|
860
|
+
else -> super.dispatchTouchEvent(event)
|
|
861
|
+
}
|
|
862
|
+
finishHeaderGesture(event)
|
|
863
|
+
return handled
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return super.dispatchTouchEvent(event)
|
|
867
|
+
}
|
|
868
|
+
|
|
516
869
|
private fun isCurrentPageTarget(target: View): Boolean {
|
|
517
870
|
if (selectedPage !in 0 until adapter.itemCount) return false
|
|
518
871
|
return isDescendant(target, adapter.pageAt(selectedPage))
|
|
@@ -612,15 +965,29 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
612
965
|
attachmentGeneration += 1
|
|
613
966
|
restoredRecyclerKeys.clear()
|
|
614
967
|
viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
|
|
968
|
+
log(
|
|
969
|
+
"host-attach generation=$attachmentGeneration inner=$selectedPage " +
|
|
970
|
+
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
971
|
+
)
|
|
615
972
|
}
|
|
616
973
|
|
|
617
974
|
override fun onDetachedFromWindow() {
|
|
618
975
|
attachmentGeneration += 1
|
|
976
|
+
headerDownEvent?.recycle()
|
|
977
|
+
headerDownEvent = null
|
|
978
|
+
headerTouchActive = false
|
|
979
|
+
forwardedRecycler = null
|
|
980
|
+
pressCancelled = false
|
|
981
|
+
nativeGestureStarted = false
|
|
619
982
|
viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
|
|
620
983
|
detachRecyclerObserver()
|
|
621
984
|
for (recycler in originalRecyclerPadding.keys.toList()) {
|
|
622
985
|
restoreRecyclerInsets(recycler)
|
|
623
986
|
}
|
|
987
|
+
log(
|
|
988
|
+
"host-detach generation=$attachmentGeneration inner=$selectedPage " +
|
|
989
|
+
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
990
|
+
)
|
|
624
991
|
super.onDetachedFromWindow()
|
|
625
992
|
}
|
|
626
993
|
|
|
@@ -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,11 +181,12 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
151
181
|
// OneKey patch: CollapsiblePagerHost always coordinates nested pagers on Android.
|
|
152
182
|
}
|
|
153
183
|
|
|
154
|
-
// The smooth shared-header ownership is implemented on iOS first.
|
|
155
184
|
override fun setNativeSmoothHeaderScrollEnabled(
|
|
156
185
|
view: CollapsiblePagerHost?,
|
|
157
186
|
value: Boolean,
|
|
158
|
-
)
|
|
187
|
+
) {
|
|
188
|
+
view?.nativeSmoothHeaderScrollEnabled = value
|
|
189
|
+
}
|
|
159
190
|
|
|
160
191
|
// OneKey patch: round Yoga header dimensions to the nearest physical pixel.
|
|
161
192
|
@ReactProp(name = "headerHeight", defaultInt = 0)
|
|
@@ -179,41 +210,91 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
179
210
|
view?.retainedPages = value ?: "[]"
|
|
180
211
|
}
|
|
181
212
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
override fun setNativeTabBarItems(view: CollapsiblePagerHost?, value: String?) {
|
|
214
|
+
view?.updateNativeTabBarItems(value)
|
|
215
|
+
}
|
|
185
216
|
|
|
186
|
-
override fun setNativeTabBarHeight(view: CollapsiblePagerHost?, value: Double)
|
|
217
|
+
override fun setNativeTabBarHeight(view: CollapsiblePagerHost?, value: Double) {
|
|
218
|
+
view ?: return
|
|
219
|
+
view.nativeTabBarHeight = value
|
|
220
|
+
view.applyNativeHeaderStyle()
|
|
221
|
+
}
|
|
187
222
|
|
|
188
223
|
override fun setNativeTabBarContentPaddingHorizontal(
|
|
189
224
|
view: CollapsiblePagerHost?,
|
|
190
225
|
value: Double,
|
|
191
|
-
)
|
|
226
|
+
) {
|
|
227
|
+
view ?: return
|
|
228
|
+
view.nativeTabBarContentPaddingHorizontal = value
|
|
229
|
+
view.applyNativeHeaderStyle()
|
|
230
|
+
}
|
|
192
231
|
|
|
193
|
-
override fun setNativeTabBarItemSpacing(view: CollapsiblePagerHost?, value: Double)
|
|
232
|
+
override fun setNativeTabBarItemSpacing(view: CollapsiblePagerHost?, value: Double) {
|
|
233
|
+
view ?: return
|
|
234
|
+
view.nativeTabBarItemSpacing = value
|
|
235
|
+
view.applyNativeHeaderStyle()
|
|
236
|
+
}
|
|
194
237
|
|
|
195
|
-
override fun setNativeTabBarFontSize(view: CollapsiblePagerHost?, value: Double)
|
|
238
|
+
override fun setNativeTabBarFontSize(view: CollapsiblePagerHost?, value: Double) {
|
|
239
|
+
view ?: return
|
|
240
|
+
view.nativeTabBarFontSize = value
|
|
241
|
+
view.applyNativeHeaderStyle()
|
|
242
|
+
}
|
|
196
243
|
|
|
197
|
-
override fun setNativeTabBarFontFamily(view: CollapsiblePagerHost?, value: String?)
|
|
244
|
+
override fun setNativeTabBarFontFamily(view: CollapsiblePagerHost?, value: String?) {
|
|
245
|
+
view ?: return
|
|
246
|
+
view.nativeTabBarFontFamily = value
|
|
247
|
+
view.applyNativeHeaderStyle()
|
|
248
|
+
}
|
|
198
249
|
|
|
199
|
-
override fun setNativeTabBarBackgroundColor(view: CollapsiblePagerHost?, value: Int?)
|
|
250
|
+
override fun setNativeTabBarBackgroundColor(view: CollapsiblePagerHost?, value: Int?) {
|
|
251
|
+
view ?: return
|
|
252
|
+
view.nativeTabBarBackgroundColor = value ?: Color.TRANSPARENT
|
|
253
|
+
view.applyNativeHeaderStyle()
|
|
254
|
+
}
|
|
200
255
|
|
|
201
|
-
override fun setNativeTabBarActiveTextColor(view: CollapsiblePagerHost?, value: Int?)
|
|
256
|
+
override fun setNativeTabBarActiveTextColor(view: CollapsiblePagerHost?, value: Int?) {
|
|
257
|
+
view ?: return
|
|
258
|
+
view.nativeTabBarActiveTextColor = value ?: Color.BLACK
|
|
259
|
+
view.applyNativeHeaderStyle()
|
|
260
|
+
}
|
|
202
261
|
|
|
203
|
-
override fun setNativeTabBarInactiveTextColor(view: CollapsiblePagerHost?, value: Int?)
|
|
262
|
+
override fun setNativeTabBarInactiveTextColor(view: CollapsiblePagerHost?, value: Int?) {
|
|
263
|
+
view ?: return
|
|
264
|
+
view.nativeTabBarInactiveTextColor = value ?: Color.GRAY
|
|
265
|
+
view.applyNativeHeaderStyle()
|
|
266
|
+
}
|
|
204
267
|
|
|
205
|
-
override fun setNativeTabBarIndicatorColor(view: CollapsiblePagerHost?, value: Int?)
|
|
268
|
+
override fun setNativeTabBarIndicatorColor(view: CollapsiblePagerHost?, value: Int?) {
|
|
269
|
+
view ?: return
|
|
270
|
+
view.nativeTabBarIndicatorColor = value ?: view.nativeTabBarActiveTextColor
|
|
271
|
+
view.applyNativeHeaderStyle()
|
|
272
|
+
}
|
|
206
273
|
|
|
207
|
-
override fun setNativeTabBarIndicatorHeight(view: CollapsiblePagerHost?, value: Double)
|
|
274
|
+
override fun setNativeTabBarIndicatorHeight(view: CollapsiblePagerHost?, value: Double) {
|
|
275
|
+
view ?: return
|
|
276
|
+
view.nativeTabBarIndicatorHeight = value
|
|
277
|
+
view.applyNativeHeaderStyle()
|
|
278
|
+
}
|
|
208
279
|
|
|
209
|
-
override fun setNativeTabBarIndicatorBottom(view: CollapsiblePagerHost?, value: Double)
|
|
280
|
+
override fun setNativeTabBarIndicatorBottom(view: CollapsiblePagerHost?, value: Double) {
|
|
281
|
+
view ?: return
|
|
282
|
+
view.nativeTabBarIndicatorBottom = value
|
|
283
|
+
view.applyNativeHeaderStyle()
|
|
284
|
+
}
|
|
210
285
|
|
|
211
|
-
override fun setNativeSubHeaderConfig(view: CollapsiblePagerHost?, value: String?)
|
|
286
|
+
override fun setNativeSubHeaderConfig(view: CollapsiblePagerHost?, value: String?) {
|
|
287
|
+
view?.updateNativeSubHeader(value)
|
|
288
|
+
}
|
|
212
289
|
|
|
213
290
|
override fun setNativeSubHeaderSelectedBackgroundColor(
|
|
214
291
|
view: CollapsiblePagerHost?,
|
|
215
292
|
value: Int?,
|
|
216
|
-
)
|
|
293
|
+
) {
|
|
294
|
+
view ?: return
|
|
295
|
+
view.nativeSubHeaderSelectedBackgroundColor = value ?: Color.TRANSPARENT
|
|
296
|
+
view.applyNativeHeaderStyle()
|
|
297
|
+
}
|
|
217
298
|
|
|
218
299
|
private fun parseStringArray(value: String?): List<String> {
|
|
219
300
|
if (value.isNullOrEmpty()) return emptyList()
|
|
@@ -255,6 +336,14 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
255
336
|
CollapsibleStateChangedEvent.EVENT_NAME,
|
|
256
337
|
MapBuilder.of("registrationName", "onCollapsibleStateChanged"),
|
|
257
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
|
+
)
|
|
258
347
|
.build()
|
|
259
348
|
.toMutableMap()
|
|
260
349
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.reactnativepagerview.event
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
7
|
+
|
|
8
|
+
class NativeHeaderPressEvent(
|
|
9
|
+
viewTag: Int,
|
|
10
|
+
private val position: Int,
|
|
11
|
+
private val key: String,
|
|
12
|
+
private val name: String,
|
|
13
|
+
) : Event<NativeHeaderPressEvent>(viewTag) {
|
|
14
|
+
override fun getEventName() = name
|
|
15
|
+
|
|
16
|
+
override fun canCoalesce() = false
|
|
17
|
+
|
|
18
|
+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
|
|
19
|
+
rctEventEmitter.receiveEvent(viewTag, eventName, serializeEventData())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private fun serializeEventData(): WritableMap = Arguments.createMap().apply {
|
|
23
|
+
putInt("position", position)
|
|
24
|
+
putString("key", key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
companion object {
|
|
28
|
+
const val TAB_EVENT_NAME = "topNativeTabPress"
|
|
29
|
+
const val SUB_HEADER_EVENT_NAME = "topNativeSubHeaderPress"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -73,7 +73,7 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
73
73
|
return React.Children.toArray(this.props.children);
|
|
74
74
|
}
|
|
75
75
|
nativeTabBarEnabled() {
|
|
76
|
-
return Platform.OS === "ios" && !!this.props.nativeTabBar?.items.length;
|
|
76
|
+
return (Platform.OS === "ios" || Platform.OS === "android") && !!this.props.nativeTabBar?.items.length;
|
|
77
77
|
}
|
|
78
78
|
dispatchNativeTabPageCommand(position, animated) {
|
|
79
79
|
const commandId = ++this.nativeTabCommandId;
|
|
@@ -169,9 +169,9 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
169
169
|
const pageKeys = pages.map(pageKey);
|
|
170
170
|
const retained = new Set(this.mountedPages(pages.length));
|
|
171
171
|
const deducedLayoutDirection = !layoutDirection || layoutDirection === "locale" ? I18nManager.isRTL ? "rtl" : "ltr" : layoutDirection;
|
|
172
|
-
const nativeTabBarEnabled = Platform.OS === "ios" && !!nativeTabBar?.items.length;
|
|
172
|
+
const nativeTabBarEnabled = (Platform.OS === "ios" || Platform.OS === "android") && !!nativeTabBar?.items.length;
|
|
173
173
|
const nativeTabBarStyle = nativeTabBar?.style;
|
|
174
|
-
const nativeSubHeaderEnabled = Platform.OS === "ios" && !!nativeSubHeader?.items.length;
|
|
174
|
+
const nativeSubHeaderEnabled = (Platform.OS === "ios" || Platform.OS === "android") && !!nativeSubHeader?.items.length;
|
|
175
175
|
const nativeSubHeaderStyle = nativeSubHeader?.style;
|
|
176
176
|
const nativeSubHeaderConfig = nativeSubHeaderEnabled ? JSON.stringify({
|
|
177
177
|
...nativeSubHeader,
|
|
@@ -65,8 +65,8 @@ export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children"
|
|
|
65
65
|
/** Measured sticky bar height. */
|
|
66
66
|
stickyHeaderHeight: number;
|
|
67
67
|
/**
|
|
68
|
-
* Lets the active
|
|
69
|
-
* Defaults to false and
|
|
68
|
+
* Lets the active native list own vertical gestures that begin on pager headers.
|
|
69
|
+
* Defaults to false and has no effect on Web.
|
|
70
70
|
*/
|
|
71
71
|
nativeSmoothHeaderScrollEnabled?: boolean;
|
|
72
72
|
/**
|
|
@@ -76,10 +76,10 @@ export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children"
|
|
|
76
76
|
*/
|
|
77
77
|
pageRetentionDistance?: number;
|
|
78
78
|
layoutDirection?: "ltr" | "rtl" | "locale";
|
|
79
|
-
/** Optional native tab bar
|
|
79
|
+
/** Optional native tab bar for iOS and Android. */
|
|
80
80
|
nativeTabBar?: CollapsiblePagerNativeTabBarConfig;
|
|
81
81
|
onNativeTabPress?: (event: CollapsiblePagerViewOnNativeTabPressEvent) => void;
|
|
82
|
-
/** Optional native secondary sticky header
|
|
82
|
+
/** Optional native secondary sticky header for iOS and Android. */
|
|
83
83
|
nativeSubHeader?: CollapsiblePagerNativeSubHeaderConfig;
|
|
84
84
|
onNativeSubHeaderPress?: (event: CollapsiblePagerViewOnNativeSubHeaderPressEvent) => void;
|
|
85
85
|
children?: React.ReactNode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.120",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"prepare": "rm -rf lib && bob build",
|
|
40
40
|
"typecheck": "tsc -b",
|
|
41
41
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
42
|
-
"test": "
|
|
42
|
+
"test": "node --test src/__tests__/*.cjs",
|
|
43
43
|
"release": "yarn prepare && npm whoami && npm publish --access public"
|
|
44
44
|
},
|
|
45
45
|
"keywords": [
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"typescript": "^5.9.2"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
+
"@onekeyfe/react-native-native-logger": "3.0.120",
|
|
70
71
|
"react": "*",
|
|
71
72
|
"react-native": "*"
|
|
72
73
|
},
|
|
@@ -115,8 +115,8 @@ export interface CollapsiblePagerViewProps
|
|
|
115
115
|
/** Measured sticky bar height. */
|
|
116
116
|
stickyHeaderHeight: number;
|
|
117
117
|
/**
|
|
118
|
-
* Lets the active
|
|
119
|
-
* Defaults to false and
|
|
118
|
+
* Lets the active native list own vertical gestures that begin on pager headers.
|
|
119
|
+
* Defaults to false and has no effect on Web.
|
|
120
120
|
*/
|
|
121
121
|
nativeSmoothHeaderScrollEnabled?: boolean;
|
|
122
122
|
/**
|
|
@@ -126,10 +126,10 @@ export interface CollapsiblePagerViewProps
|
|
|
126
126
|
*/
|
|
127
127
|
pageRetentionDistance?: number;
|
|
128
128
|
layoutDirection?: "ltr" | "rtl" | "locale";
|
|
129
|
-
/** Optional native tab bar
|
|
129
|
+
/** Optional native tab bar for iOS and Android. */
|
|
130
130
|
nativeTabBar?: CollapsiblePagerNativeTabBarConfig;
|
|
131
131
|
onNativeTabPress?: (event: CollapsiblePagerViewOnNativeTabPressEvent) => void;
|
|
132
|
-
/** Optional native secondary sticky header
|
|
132
|
+
/** Optional native secondary sticky header for iOS and Android. */
|
|
133
133
|
nativeSubHeader?: CollapsiblePagerNativeSubHeaderConfig;
|
|
134
134
|
onNativeSubHeaderPress?: (
|
|
135
135
|
event: CollapsiblePagerViewOnNativeSubHeaderPressEvent
|
|
@@ -248,7 +248,10 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
private nativeTabBarEnabled() {
|
|
251
|
-
return
|
|
251
|
+
return (
|
|
252
|
+
(Platform.OS === "ios" || Platform.OS === "android") &&
|
|
253
|
+
!!this.props.nativeTabBar?.items.length
|
|
254
|
+
);
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
private dispatchNativeTabPageCommand(position: number, animated: boolean) {
|
|
@@ -393,10 +396,12 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
393
396
|
: "ltr"
|
|
394
397
|
: layoutDirection;
|
|
395
398
|
const nativeTabBarEnabled =
|
|
396
|
-
Platform.OS === "ios"
|
|
399
|
+
(Platform.OS === "ios" || Platform.OS === "android") &&
|
|
400
|
+
!!nativeTabBar?.items.length;
|
|
397
401
|
const nativeTabBarStyle = nativeTabBar?.style;
|
|
398
402
|
const nativeSubHeaderEnabled =
|
|
399
|
-
Platform.OS === "ios"
|
|
403
|
+
(Platform.OS === "ios" || Platform.OS === "android") &&
|
|
404
|
+
!!nativeSubHeader?.items.length;
|
|
400
405
|
const nativeSubHeaderStyle = nativeSubHeader?.style;
|
|
401
406
|
const nativeSubHeaderConfig = nativeSubHeaderEnabled
|
|
402
407
|
? JSON.stringify({
|